"""
Dundun CLI 主入口
集成会话管理、Agent 系统、命令处理等功能

注意：此模块需要通过 pip install -e . 安装后使用
"""

import asyncio
import os
import sys
from typing import Optional, Dict, Any, Tuple
from pathlib import Path

import typer
from dotenv import load_dotenv


def _clear_terminal_on_startup() -> None:
    """尽量清屏并清空 scrollback：仅影响客户端启动时所处的终端。

    目标效果接近用户在 shell 里手动执行 `clear`（部分终端的 clear 不会清 scrollback）。
    - 先尝试 Rich 的 clear（兼容性较好）
    - 再发送 ANSI 清屏/归位
    - 最后发送清空 scrollback 的序列（iTerm2/部分终端支持；不支持则无害）
    """
    try:
        if sys.stdout is None or not sys.stdout.isatty():
            return

        # 1) Rich clear
        try:
            from rich.console import Console

            Console().clear()
        except Exception:
            pass

        # 2) Clear screen + cursor home
        try:
            sys.stdout.write("\x1b[2J\x1b[H")
        except Exception:
            return

        # 3) Clear scrollback buffer (xterm extension; supported by e.g. iTerm2)
        try:
            sys.stdout.write("\x1b[3J")
        except Exception:
            pass

        try:
            sys.stdout.flush()
        except Exception:
            pass
    except Exception:
        return


# ============================================================================
# 环境配置
# ============================================================================

# 加载环境变量 - 按优先级从多个位置加载
def _load_env_files():
    """加载 .env 文件"""
    # 当前工作目录
    cwd_env = Path.cwd() / ".env"
    if cwd_env.exists():
        load_dotenv(cwd_env)
        return

    # 默认调用（尝试自动查找）
    load_dotenv()

_load_env_files()

from core.config import load_config, load_settings
from core.logger import log_event, set_log_side
from rich.text import Text

# 接口
from integrations.cli.commands import get_command_handler
from integrations.cli.renderer import get_tui, get_streaming_renderer, DundunApp
from integrations.cli.styles import Styles

# 事件处理器（新增）
from integrations.cli.event_dispatcher import EventDispatcher
from integrations.cli.render_handler import RenderHandler
from integrations.cli.interaction_handler import InteractionHandler

# WebSocket 客户端
from integrations.cli.websocket_client import WebSocketClient

# 应用状态管理
try:
    from main import get_app_state
except ImportError:
    raise ImportError("无法导入 main 模块，请确保项目已正确安装")


# ============================================================================
# 全局状态
# ============================================================================

# 标记连接错误，用于在 Textual 退出后抛出异常
_connection_error: Optional[Exception] = None


app = typer.Typer(
    help="Dundun Agent - AI 编程助手",
    add_completion=False,
    no_args_is_help=False
)


class RuntimeContext:
    """运行时上下文（从服务端获取，用于 UI 展示）

    所有字段与服务端 Session 状态对应，是客户端唯一的状态源。
    TUI 状态栏直接从本对象读取数据进行渲染。
    """

    def __init__(self):
        self.agent_type: str = "general"
        self.model_name: str = ""
        self.provider: str = ""
        self.available_models: list = []
        self.auto_mode: bool = False  # 自动模式（跳过所有权限确认）
        # 会话信息
        self.session_id: str = ""
        self.working_path: str = ""  # 服务端工作目录（原 SERVER_WORKING_PATH）
        self.git_branch: str = ""    # 服务端 git 分支（非 git 仓库为空字符串）
        # 上下文窗口使用情况
        self.context_tokens: int = 0
        self.context_max: int = 0
        self.context_percent: float = 0.0

    def update(self, **kwargs):
        """更新运行时上下文"""
        for key, value in kwargs.items():
            if hasattr(self, key):
                setattr(self, key, value)


class CLIContext:
    """CLI 上下文（纯客户端状态）"""

    def __init__(self):
        # 会话 ID
        self.session_id: Optional[str] = None
        # WebSocket 客户端
        self.ws_client: Optional[WebSocketClient] = None
        # 服务端 URL（用于 HTTP 调用）
        self.server_url: str = ""
        # 调试模式
        self.debug_mode: bool = False
        # 运行时上下文（从服务端获取）
        self.runtime: RuntimeContext = RuntimeContext()

    def to_dict(self) -> Dict[str, Any]:
        """转换为字典（用于命令处理）"""
        return {
            "session_id": self.session_id,
            "current_agent": self.runtime.agent_type,
            "model_name": self.runtime.model_name,
            "provider": self.runtime.provider,
            "available_models": self.runtime.available_models,
            "debug_mode": self.debug_mode,
            "server_url": self.server_url,
            "auto_mode": self.runtime.auto_mode,
        }


async def process_user_input(
    ctx: CLIContext,
    user_input: Any,
    tui,
    command_handler,
    renderer,
) -> bool:
    """
    处理用户输入

    Returns:
        bool: 是否继续主循环
    """
    # 允许 Textual 输入控件提交结构化数据：{"text": str, "data": dict}
    structured_data = None
    if isinstance(user_input, dict):
        structured_data = user_input.get("data")
        user_input = user_input.get("text") or ""

    # 子菜单结构化 action（避免拼字符串再解析）
    # 通过 ActionRouter 解耦，CLI 不再知道具体 action 名称
    if structured_data and isinstance(structured_data, dict) and "action" in structured_data:
        from integrations.cli.commands import get_action_router

        if await get_action_router().dispatch(structured_data, ctx):
            return True

    # 检查是否是命令
    # 注意：如果命令不存在（result is None），需要继续走“正常对话”分支，把以 / 开头的输入作为普通 prompt 提交到后端。
    if user_input.startswith("/"):
        result = await command_handler.execute(user_input, ctx.to_dict())

        # 命令不存在（返回 None），作为普通 prompt 处理（不在这里 return）
        if result is None:
            result = False

        if result:
            # 如果需要填充输入框（命令需要参数但没有提供）
            if result.fill_input and tui._app:
                tui._app.fill_input(result.fill_input)
                return True

            # 打印命令结果
            if result.message:
                tui.print_command_result(result.message, result.success)

            # 处理命令数据
            if result.data:
                # 作为用户输入处理（如 /init 命令）
                if "as_user_input" in result.data:
                    user_message = result.data["as_user_input"]
                    # 记录日志
                    log_event("init_command_triggered", session_id=ctx.session_id)
                    # 设置为处理状态
                    app_state = get_app_state()
                    app_state.set_processing_state()
                    # 发送消息到服务器
                    await ctx.ws_client.send_message(user_message)
                    renderer.start_processing()
                    return True

                # 切换 Agent
                if "switch_agent" in result.data:
                    new_agent_name = result.data["switch_agent"]
                    # WebSocket 模式：发送切换 Agent 消息
                    await ctx.ws_client.switch_agent(new_agent_name)
                    ctx.runtime.agent_type = new_agent_name
                    tui.print_success(f"已切换到 {new_agent_name} 模式")
                    # 更新状态栏
                    tui.update_status_bar()

                # 切换模型
                if "switch_model" in result.data:
                    model_name = result.data["switch_model"]
                    await ctx.ws_client.switch_model(model_name)
                    ctx.runtime.model_name = model_name
                    msg = Text()
                    msg.append("⏺", style=Styles.SWITCH_MODEL_ICON)
                    msg.append(f" 已切换到模型 {model_name}")
                    tui._write_safe(msg)
                    # 更新状态栏
                    tui.update_status_bar()

                # 设置自动模式
                if "set_auto_mode" in result.data:
                    enabled = result.data["set_auto_mode"]
                    await ctx.ws_client.set_auto_mode(enabled)
                    ctx.runtime.auto_mode = enabled
                    # 更新状态栏显示 AUTO 标识
                    tui.update_status_bar()

                # 切换会话
                if "switch_session_id" in result.data:
                    await _switch_to_session(ctx, result.data["switch_session_id"])
                    return True

                # 新建会话
                if "new_session_id" in result.data:
                    ctx.session_id = result.data["new_session_id"]
                    ctx.runtime.session_id = ctx.session_id
                    # 更新状态栏
                    tui.update_status_bar()

                # 调试模式
                if "debug_mode" in result.data:
                    ctx.debug_mode = result.data["debug_mode"]
                    tui.debug_mode = ctx.debug_mode

                # 查询工具
                if "query_tools" in result.data:
                    await ctx.ws_client.query_tools()

                # 初始化项目
                if "init" in result.data:
                    force = result.data.get("force", False)
                    await ctx.ws_client.init_project(force)

                # 查询 skills
                if "query_skills" in result.data:
                    await ctx.ws_client.query_skills()

                # 请求压缩上下文
                if "compact" in result.data:
                    # 启动手动压缩的 spinner 动画
                    renderer.on_manual_compact_start()
                    await ctx.ws_client.context_compact()

                # 新建会话并清空输出
                if "new_session" in result.data:
                    # 清空输出区
                    tui.clear_output()
                    # 发送 switch_session，空 session_id 让服务端生成新会话
                    await ctx.ws_client.switch_session("", ctx.runtime.agent_type)
                    # connected 事件到达后会更新 session_id 和状态栏

            # 检查是否退出
            if result.should_exit:
                return False

            return True

    # 空输入
    if not user_input.strip():
        return True

    # 正常对话
    try:
        app_state = get_app_state()

        # 设置为处理状态
        app_state.set_processing_state()

        # 记录用户输入
        log_event("user_input", session_id=ctx.session_id, input=user_input)
        log_event("process_start", agent=ctx.runtime.agent_type, input=user_input)

        # WebSocket 模式：发送消息到服务器
        await ctx.ws_client.send_message(user_input)

        # 通知 renderer 开始处理，阻止下一次输入
        renderer.start_processing()

        # 响应会通过 WebSocket 事件处理器接收


    except KeyboardInterrupt:
        tui.print_warning("已中断")
        # 恢复到输入状态
        get_app_state().set_input_state()
    except Exception as e:
        # 执行错误，记录日志并打印
        import traceback
        error_details = traceback.format_exception(type(e), e, e.__traceback__)
        error_msg = "".join(error_details)
        log_event("user_input_error", error=str(e), traceback=error_msg)
        tui.print_error(f"执行错误: {str(e)}")
        if ctx.debug_mode:
            import traceback as tb
            tb.print_exc()
        # 恢复到输入状态
        get_app_state().set_input_state()

    # 注意：不要在这里设置 input_state，因为 WebSocket 是异步的
    # 状态会在 handle_response_end_event 中被设回 input
    return True


async def _switch_to_session(ctx: CLIContext, session_id: str):
    """切换到指定会话（不断开 WS 连接）"""
    # 更新客户端 session_id
    ctx.session_id = session_id
    ctx.runtime.session_id = session_id
    ctx.ws_client.session_id = session_id

    # 发送 switch_session 消息，服务端会回复 connected 事件
    await ctx.ws_client.switch_session(session_id, ctx.runtime.agent_type)
    # connected 事件会触发 RuntimeContext 更新和状态栏刷新


async def _register_action_handlers(ctx: CLIContext):
    """Register action handlers with the global ActionRouter.

    This should be called once during CLI initialization.
    """
    from integrations.cli.commands import get_action_router

    async def handle_switch_session(data: dict, ctx: CLIContext):
        """Handle switch_session action."""
        session_id = data.get("session_id", "")
        if session_id:
            await _switch_to_session(ctx, session_id)

    router = get_action_router()
    router.register("switch_session", handle_switch_session)


async def main_loop(ctx: CLIContext):
    """主循环"""
    tui = get_tui()
    renderer = get_streaming_renderer()
    command_handler = get_command_handler()
    app_state = get_app_state()

    # WebSocket 事件处理器已在连接时注册

    # 主循环
    loop_count = 0

    while True:
        loop_count += 1
        if ctx.debug_mode:
            tui._write_safe(Text(f"--- 循环 #{loop_count} ---", style="dim"))

        try:
            # 等待 Agent 处理完成后才允许输入
            if ctx.debug_mode:
                tui._write_safe(Text("等待 input_ready...", style="dim"))
            await renderer.input_ready.wait()
            if ctx.debug_mode:
                tui._write_safe(Text("input_ready 已设置，可以获取输入", style="dim"))

            # 设置为用户输入状态
            app_state.set_input_state()

            # 获取用户输入（通过 Textual Input 组件）
            user_input = await tui.get_user_input_async()

            # Textual 模式下不需要清除行（Input 在独立区域）

            # 如果有输入内容，重新渲染为斜体
            # 兼容结构化输入：{"text": str, "data": dict}
            _render_text = user_input
            if isinstance(user_input, dict):
                _render_text = user_input.get("text") or ""
            if _render_text and isinstance(_render_text, str) and _render_text.strip():
                tui.render_user_input(_render_text)

            if ctx.debug_mode:
                tui._write_safe(Text(f"收到输入: {repr(user_input)}", style="dim"))

            # 处理输入
            should_continue = await process_user_input(
                ctx, user_input, tui, command_handler, renderer
            )

            if ctx.debug_mode:
                tui._write_safe(Text(f"should_continue: {should_continue}", style="dim"))

            if not should_continue:
                if ctx.debug_mode:
                    tui._write_safe(Text("退出主循环", style="dim"))
                break

        except asyncio.CancelledError:
            if ctx.debug_mode:
                tui._write_safe(Text("输入任务被取消", style="dim"))
            continue
        except KeyboardInterrupt:
            pass
        except EOFError:
            if ctx.debug_mode:
                tui._write_safe(Text("收到 EOFError，退出", style="dim"))
            break
        except ConnectionError as e:
            # 连接相关错误需要退出，记录日志并退出
            log_event("connection_error", error=str(e))
            tui.print_error(f"连接错误: {e}")
            break
        except Exception as e:
            # 其他未知错误，记录完整日志并退出
            import traceback
            error_details = traceback.format_exception(type(e), e, e.__traceback__)
            error_msg = "".join(error_details)
            log_event("unknown_error", error=str(e), traceback=error_msg)
            tui.print_error(f"未知错误: {str(e)}")
            print(f"\n✗ Error: 未知错误: {e}", file=sys.stderr, flush=True)
            print(f"详细信息:\n{error_msg}", file=sys.stderr, flush=True)
            if ctx.debug_mode:
                import traceback as tb
                tb.print_exc()
            # 退出主循环
            break


async def setup_websocket_handlers(ctx: CLIContext, tui, renderer):
    """
    设置 WebSocket 事件处理器 - 使用事件分发器

    职责：
    - 将 WebSocket 事件分发给 RenderHandler 和 InteractionHandler
    - 处理权限请求和交互请求（需要用户输入）
    - 处理连接断开和错误
    """
    from core.logger import log_event

    # 获取应用状态
    app_state = get_app_state()

    # 创建事件处理器
    render_handler = RenderHandler(ctx.ws_client, tui, renderer, app_state)
    interaction_handler = InteractionHandler(ctx.ws_client, tui, renderer, app_state)

    # 创建事件分发器
    event_dispatcher = EventDispatcher(render_handler, interaction_handler)

    def _is_subagent_session_id(session_id: Any) -> bool:
        """判断 session_id 是否为子 Agent 会话：<parent>.<8hex>"""
        if not isinstance(session_id, str) or "." not in session_id:
            return False
        last = session_id.rsplit(".", 1)[-1]
        return len(last) == 8 and all(c in "0123456789abcdef" for c in last.lower())

    def _maybe_route_subtask_event(event: Dict[str, Any]) -> bool:
        """如果是子任务事件则转发并返回 True（调用方应短路原逻辑）。"""
        # subtask_session_id 在 event.data 内部（由 task_tool 转发时添加）
        data = event.get("data") or {}
        subtask_id = data.get("subtask_session_id")
        if not subtask_id:
            return False
        # 仅当该 subtask_id 已在子任务状态表中时才路由
        if subtask_id in renderer._subtask_states:
            renderer.on_subtask_process(subtask_id, event)
            return True
        return False

    # 触发上下文信息更新的事件白名单
    _CONTEXT_UPDATE_EVENTS = {"tool_call_end", "text_chunk_complete", "model_switched", "agent_switched", "connected", "compact_result"}

    async def _route_event(event: Dict[str, Any]):
        """统一事件路由入口：在这里做子任务转发与按 type 分发。"""
        event_type = event.get("type", "")
        log_event("client_received_event", event_type=event_type, data=event.get("data"))

        if event_type == "connected":
            data = event.get("data", {})
            server_session_id = data.get("session_id", "")
            if server_session_id:
                ctx.session_id = server_session_id
                ctx.runtime.session_id = server_session_id
                if ctx.ws_client:
                    ctx.ws_client.session_id = server_session_id
                # 更新 RuntimeContext 中的工作路径和会话信息
                working_path = data.get("working_path", "")
                if working_path:
                    ctx.runtime.working_path = working_path
                git_branch = data.get("git_branch", "")
                ctx.runtime.git_branch = git_branch
                model_name = data.get("model_name", "")
                agent_type = data.get("agent_type", "")
                if model_name:
                    ctx.runtime.model_name = model_name
                if agent_type:
                    ctx.runtime.agent_type = agent_type
                tui.update_status_bar()

            # 异步预加载会话列表（不阻塞 UI）
            try:
                from integrations.cli.widgets import UserInput
                user_input = tui._app.query_one("#user-input", UserInput)
                asyncio.create_task(user_input._fetch_sessions())
            except Exception:
                pass

        if event_type == "auto_mode_changed":
            data = event.get("data", {})
            ctx.runtime.auto_mode = data.get("auto_mode", False)
            # 更新状态栏显示 AUTO 标识
            tui.update_status_bar()

        # 生命周期事件和交互类事件不应被子任务路由"吞掉"
        if event_type not in {"permission_request", "interaction_request", "subtask_started", "subtask_completed"}:
            if _maybe_route_subtask_event(event):
                return

        # 使用事件分发器分发
        await event_dispatcher.dispatch(event)

        # 白名单事件触发上下文信息更新
        if event_type in _CONTEXT_UPDATE_EVENTS:
            await _update_context_info()

    async def _update_context_info():
        """通过 HTTP 查询会话信息并更新 RuntimeContext + 状态栏"""
        try:
            data = await ctx.ws_client.get_context_info()
            # 全部写入 RuntimeContext
            ctx.runtime.context_tokens = data.get("token_count", 0)
            ctx.runtime.context_max = data.get("max_tokens", 0)
            ctx.runtime.context_percent = data.get("percent", 0)
            model_name = data.get("model_name", "")
            if model_name:
                ctx.runtime.model_name = model_name
            agent_type = data.get("agent_type", "")
            if agent_type:
                ctx.runtime.agent_type = agent_type
            ctx.runtime.auto_mode = data.get("auto_mode", False)
            session_id = data.get("session_id", "")
            if session_id:
                ctx.runtime.session_id = session_id
            working_path = data.get("working_path", "")
            if working_path:
                ctx.runtime.working_path = working_path
            git_branch = data.get("git_branch", "")
            ctx.runtime.git_branch = git_branch
            tui.update_status_bar()
        except Exception as e:
            log_event("context_info_fetch_error", error=str(e))

    # ========== 注册事件处理器 ==========

    def _register_all_ws_events():
        """统一注册所有服务端事件，然后由 _route_event 进行路由。"""
        # 允许在切换/重连后更新处理器的 ws_client 引用
        render_handler.ws_client = ctx.ws_client
        interaction_handler.ws_client = ctx.ws_client
        ctx.ws_client.on_disconnect(interaction_handler.handle_disconnect)
        ctx.ws_client.on_error(interaction_handler.handle_ws_error)

        for event_type in event_dispatcher.event_types:
            ctx.ws_client.on(event_type, _route_event)

    _register_all_ws_events()

    # 返回注册函数，供切换客户端时重新注册
    return _register_all_ws_events


async def async_run(
    config_path: str,
    model: Optional[str],
    agent_name: str,
    session_id: Optional[str],
    debug: bool,
    use_websocket: bool = False,
    ws_url: str = "ws://localhost:8367",
):
    """异步运行 - 在 Textual App 内部执行"""
    _clear_terminal_on_startup()
    # 预先测试连接，失败则直接退出
    # 注意：不预先测试连接，让服务端生成 session_id
    # 连接测试会在 WebSocketClient.connect() 中进行

    tui = get_tui()
    tui.debug_mode = debug

    # 业务逻辑协程（在 Textual App 事件循环内运行）
    async def _run_business_logic(textual_app: DundunApp):
        """业务逻辑"""
        # 等待 App 完全挂载
        await asyncio.sleep(0.1)

        # Textual 挂载后再清一次屏，避免 alternate screen 切换导致视觉上未清
        _clear_terminal_on_startup()

        # 先展示欢迎信息（connected 事件到达后会刷新）
        tui.print_welcome()

        # 创建上下文
        ctx = CLIContext()
        ctx.debug_mode = debug
        await _register_action_handlers(ctx)

        # 注入 RuntimeContext 引用到 DundunApp（状态栏从此读取）
        if tui._app:
            tui._app._runtime = ctx.runtime

        # 1. 设置日志标识
        set_log_side("CLIENT")

        # 2. 加载配置
        try:
            config = load_config(config_path)

            # 确定当前模型名：--model 参数 > setting.json > config.provider.default
            if model:
                model_name_key = model
            else:
                settings = load_settings()
                saved_model = settings.get("default_model")
                if saved_model:
                    model_name_key = saved_model
                else:
                    model_name_key = config.provider.default
            if not model_name_key:
                # 兜底：留空，由服务端决定默认模型
                model_name_key = ""

            # 设置初始环境信息
            if model_name_key and "/" in model_name_key:
                ctx.runtime.provider = model_name_key.split("/", 1)[0]
                ctx.runtime.model_name = model_name_key
            elif model_name_key:
                ctx.runtime.model_name = model_name_key
                ctx.runtime.provider = ""
            else:
                ctx.runtime.model_name = ""
                ctx.runtime.provider = ""
            ctx.runtime.agent_type = agent_name
            # available_models 在连接服务端后通过 HTTP API 获取
            ctx.runtime.available_models = []

        except Exception as e:
            # 配置加载失败，打印错误并记录日志，然后退出
            import traceback
            error_details = traceback.format_exception(type(e), e, e.__traceback__)
            error_msg = "".join(error_details)
            tui.print_error(f"配置加载失败: {e}")
            log_event("config_load_error", error=str(e), traceback=error_msg)
            # 打印详细错误信息
            print(f"\n✗ Error: 配置加载失败: {e}", file=sys.stderr, flush=True)
            print(f"详细信息:\n{error_msg}", file=sys.stderr, flush=True)
            # 强制退出程序
            os._exit(1)

        # 3. 会话 ID 由服务端生成，客户端先不设置
        # 客户端会在收到 connected 事件后从服务端获取 session_id
        ctx.session_id = ""  # 临时为空，连接成功后更新

        # 4. 连接 WebSocket 服务器
        if not use_websocket:
            tui.print_error("本地模式已废弃，请使用: python -m src.main")
            return

        try:
            ctx.ws_client = WebSocketClient(ws_url, ctx.session_id, ctx.runtime.agent_type)
            # 设置 HTTP 服务端地址（用于 HTTP API 调用）
            ctx.server_url = ws_url.replace("ws://", "http://").replace("wss://", "https://")
            await ctx.ws_client.connect()
            log_event("client_connected", session_id=ctx.session_id)

            # 先注册事件处理器，再启动接收循环（确保 connected 事件能被处理）
            renderer = get_streaming_renderer()
            ctx._reregister_ws_events = await setup_websocket_handlers(ctx, tui, renderer)

            # 启动接收循环（在后台运行）
            receive_task = asyncio.create_task(ctx.ws_client.receive_loop())
            log_event("websocket_connected", session_id=ctx.session_id)

            # 设置 server_url 到 UserInput（用于命令菜单获取会话列表）
            try:
                from integrations.cli.widgets import UserInput
                user_input = tui._app.query_one("#user-input", UserInput)
                user_input._server_url = ctx.server_url
            except Exception:
                pass

        except Exception as e:
            # 连接失败，打印完整错误信息并优雅退出
            import traceback
            error_details = traceback.format_exception(type(e), e, e.__traceback__)
            error_msg = "".join(error_details)
            print(f"\n✗ Error: 连接服务器失败: {e}", file=sys.stderr, flush=True)
            print(f"详细信息:\n{error_msg}", file=sys.stderr, flush=True)

            # 显示错误信息到 TUI（如果 App 已启动）
            try:
                tui.print_error(f"连接服务器失败: {e}")
            except Exception:
                pass

            # 使用 Event 来通知 App 退出
            app = tui.app
            if app and hasattr(app, '_business_done'):
                # 设置错误状态
                app._business_error = e
                # 触发退出
                app.exit()
            else:
                # 如果 App 还没准备好，强制退出
                os._exit(1)
            return

        # 设置 Ctrl+C 中断回调（通过 Textual action_quit 覆盖）
        app_state = get_app_state()
        renderer = get_streaming_renderer()

        def handle_interrupt():
            """处理 Ctrl+C 中断（由 Textual action_quit 调用）"""
            current_app_state = get_app_state()
            log_event("ctrl_c_interrupt", is_processing=current_app_state.is_processing)

            if current_app_state.is_processing:
                # 处理状态下，发送中断请求
                current_app_state.request_interrupt()
                # 立即停止所有动画和 live 行（防止残留和重复显示）
                renderer._cleanup_all()
                tui.print_info("正在中断...")
                asyncio.create_task(ctx.ws_client.interrupt())
                log_event("interrupt_requested_by_user")
            else:
                # 输入状态下，提示用户使用 /exit
                tui.print_info("使用 /exit 退出")

        textual_app.set_interrupt_callback(handle_interrupt)
        log_event("interrupt_callback_registered")

        # 设置模型列表和状态栏初始值
        textual_app.update_status_bar()

        # 从服务端获取可用模型列表
        try:
            models_data = await ctx.ws_client.get_model_list()
            ctx.runtime.available_models = [
                f"{m['provider']}/{m['name']}" for m in models_data
            ]
        except Exception:
            ctx.runtime.available_models = []
        textual_app.update_available_models(ctx.runtime.available_models)

        # 5. 运行主循环
        try:
            await main_loop(ctx)
        finally:
            # 清理
            if ctx.ws_client:
                await ctx.ws_client.disconnect(reason="shutdown")
                log_event("client_disconnected", session_id=ctx.session_id)

        # 退出 Textual App
        textual_app.exit()

    # 检查是否有连接错误需要抛出
    if _connection_error:
        raise _connection_error

    # 创建带业务逻辑回调的 Textual App 子类
    class _AppWithLogic(DundunApp):
        def __init__(self):
            super().__init__()
            self._business_error: Optional[Exception] = None

        def on_mount(self) -> None:
            super().on_mount()
            # 创建事件用于等待业务逻辑完成
            self._business_done = asyncio.Event()
            task = asyncio.create_task(_run_business_logic(self))
            # 任务完成后检查是否有异常
            task.add_done_callback(self._check_task_exception)

        def _check_task_exception(self, task: asyncio.Task):
            """检查业务逻辑任务是否有异常"""
            try:
                task.result()
            except Exception as e:
                self._business_error = e
                # 连接错误立即强制退出
                if "连接" in str(e) or isinstance(e, ConnectionError):
                    print(f"\n✗ Error: 连接服务器失败: {e}", file=sys.stderr, flush=True)
                    os._exit(1)
            finally:
                # 标记业务逻辑完成
                if hasattr(self, '_business_done'):
                    self._business_done.set()

    textual_app = _AppWithLogic()
    tui.app = textual_app

    # 运行 Textual App（这会接管终端）
    # 启用鼠标支持，以便滚轮可滚动主输出区（大多数终端可通过 Shift+拖拽 进行文本选择）
    await textual_app.run_async()

    # 等待业务逻辑任务完成
    await textual_app._business_done.wait()

    # 检查业务逻辑是否有未捕获的异常，有则重新抛出
    if textual_app._business_error:
        raise textual_app._business_error


@app.callback(invoke_without_command=True)
def main(
    ctx: typer.Context,
    config_path: str = typer.Option("config.json", "--config", "-c", help="配置文件名"),
    model: str = typer.Option(None, "--model", "-m", help="覆盖配置中的模型"),
    agent: str = typer.Option(
        "build", "--agent", "-a", help="Agent 类型: build, plan, general, explore"
    ),
    session: str = typer.Option(None, "--session", "-s", help="指定会话 ID"),
    debug: bool = typer.Option(False, "--debug", "-d", help="调试模式"),
    websocket: bool = typer.Option(
        False, "--websocket", "-w", help="使用 WebSocket 模式连接到服务器"
    ),
    ws_url: str = typer.Option(
        "ws://localhost:8367", "--ws-url", help="WebSocket 服务器地址"
    ),
):
    """
    Dundun Agent - AI 编程助手

    直接运行 dundun 启动会话，或使用子命令。

    Agent 类型:
    - general: 通用助手（默认）
    - build: 代码开发专用
    - plan: 规划模式（只读）
    - explore: 代码探索
    """
    # 如果调用了子命令，不执行默认行为
    if ctx.invoked_subcommand is not None:
        return

    asyncio.run(async_run(
        config_path=config_path,
        model=model,
        agent_name=agent,
        session_id=session,
        debug=debug,
        use_websocket=websocket,
        ws_url=ws_url,
    ))


@app.command(name="version")
def version():
    """显示版本信息"""
    tui = get_tui()
    tui._write_safe(Text.from_markup("[bold]Dundun Agent[/bold] v1.1.2"))
    tui._write_safe(Text.from_markup("[dim]Python 版本的 AI 编程助手[/dim]"))


if __name__ == "__main__":
    app()
