from __future__ import annotations

import asyncio
from pathlib import Path
from typing import Optional

import typer

from aicd.cli_encoding import configure_cli_utf8
from aicd.env_probe import refresh_runtime_env_file
from aicd.agent.loop import merge_env_api_key, merge_env_integration_tokens
from aicd.home import init_home
from aicd.network_policy import apply_cli_block_external_network_flag
from aicd.runtime import build_runtime, persist_config
from aicd.setup_wizard import (
    run_first_time_setup,
    runtime_llm_ready,
    should_prompt_setup,
    skip_first_time_setup_interactive,
)
from aicd.tui_compat import ensure_windows_vt_mode_early, tui_use_mouse
from aicd.ui.cli_headless import (
    console_mode_requested_explicit,
    run_console_chat_loop,
    run_noui_ai_turn,
)
from aicd.ui.gui_tk import run_gui_app
from aicd.ui.tui import AicdTui
from aicd.version import VERSION

app = typer.Typer(
    help=(
        "Aicd — 命令行 AI Agent（开发/文档）。"
        "快捷：aicd --ai … 与 aicd tui --ai … 相同（另可在顶层使用 --work/--noui 等，见 --help）。"
    ),
    invoke_without_command=True,
    no_args_is_help=True,
    context_settings={"help_option_names": ["--help", "-?"]},
)


def _run_tui_entry(
    *,
    home: Optional[Path],
    work: Optional[Path],
    out: Optional[Path],
    ai: Optional[str],
    noui: bool,
    skip_setup: bool,
    console: bool = False,
    no_plugins: bool = False,
    plugin_policy: Optional[str] = None,
) -> None:
    """tui 子命令与顶层快捷选项共用。"""
    use_console = console_mode_requested_explicit(console)
    if noui and not (ai and str(ai).strip()):
        typer.echo("错误: --noui 必须与 --ai 同时使用（无界面模式下必须指定任务内容）", err=True)
        raise typer.Exit(code=2)
    if noui and use_console:
        typer.echo("错误: --noui 与 --console 不能同时使用", err=True)
        raise typer.Exit(code=2)

    async def _run() -> None:
        ctx = await build_runtime(
            home=home,
            skip_first_setup=skip_setup,
            setup_tty_relaxed=not skip_setup,
            cli_no_plugins=no_plugins,
            plugin_policy_override=plugin_policy,
        )
        if (
            not skip_setup
            and not noui
            and not use_console
            and not runtime_llm_ready(ctx.config)
            and not should_prompt_setup(relaxed=True)
        ):
            typer.secho(
                "未检测到可交互终端（stdin 非 TTY），已跳过首次 LLM 向导。"
                "请在本机终端执行:  aicd init\n"
                "或设置环境变量 OPENAI_API_KEY / DASHSCOPE_API_KEY，或在 config.yaml 填写 llm.apiKey。",
                fg=typer.colors.YELLOW,
                err=True,
            )
        if work is not None:
            ok, msg = ctx.set_ai_workspace(str(work))
            if not ok:
                typer.echo(f"无法设置 AI 工作目录: {msg}", err=True)
                raise typer.Exit(code=1)
        if out is not None:
            ok, msg = ctx.set_ai_output_dir(str(out))
            if not ok:
                typer.echo(f"无法设置 AI 输出目录: {msg}", err=True)
                raise typer.Exit(code=1)
        if use_console:
            code = await run_console_chat_loop(
                ctx, str(ai).strip() if ai else None
            )
            if code != 0:
                raise typer.Exit(code=code)
            return
        if noui:
            code = await run_noui_ai_turn(ctx, str(ai).strip())
            if code != 0:
                raise typer.Exit(code=code)
            return
        bootstrap = str(ai).strip() if ai else None
        ensure_windows_vt_mode_early()
        tui_app = AicdTui(ctx, bootstrap_user_message=bootstrap or None)
        await tui_app.run_async(mouse=tui_use_mouse())

    asyncio.run(_run())


def _run_gui_entry(
    *,
    home: Optional[Path],
    work: Optional[Path],
    out: Optional[Path],
    ai: Optional[str],
    skip_setup: bool,
    no_plugins: bool = False,
    plugin_policy: Optional[str] = None,
) -> None:
    run_gui_app(
        home=home,
        work=work,
        out=out,
        skip_setup=skip_setup,
        bootstrap_ai=str(ai).strip() if ai else None,
        no_plugins=no_plugins,
        plugin_policy=plugin_policy,
    )


@app.callback()
def main(
    ctx: typer.Context,
    ai: Optional[str] = typer.Option(
        None,
        "--ai",
        help="快捷：等同 aicd tui --ai；启动后自动向主 AI 发送该内容（批处理/脚本常用）",
    ),
    noui: bool = typer.Option(
        False,
        "--noui",
        help="快捷：等同 aicd tui --noui（须同时提供 --ai）",
    ),
    work: Optional[Path] = typer.Option(
        None,
        "--work",
        "-w",
        help="快捷：等同 aicd tui --work",
    ),
    out: Optional[Path] = typer.Option(
        None,
        "--out",
        "-o",
        help="快捷：等同 aicd tui --out",
    ),
    home: Optional[Path] = typer.Option(
        None,
        "--home",
        help="快捷：等同 aicd tui --home（HOME 根目录）",
    ),
    skip_setup: bool = typer.Option(
        False,
        "--skip-setup",
        help="快捷：等同 aicd tui --skip-setup",
    ),
    console: bool = typer.Option(
        False,
        "--console",
        help="快捷：等同 aicd tui --console（传统纯文本多轮，无 Textual）",
    ),
    gui: bool = typer.Option(
        False,
        "--gui",
        help="快捷：等同 aicd gui（Tk 图形窗口；与 --noui / --console 互斥）",
    ),
    no_plugins: bool = typer.Option(
        False,
        "--no-plugins",
        help="启动时不加载任何 HOME 插件（仍可用 plugin_load / plugin_reload）",
    ),
    plugin_policy: Optional[str] = typer.Option(
        None,
        "--plugin-policy",
        help="覆盖 AICD_PLUGIN_POLICY（如 off、explicit、full）",
    ),
    block_external_network: bool = typer.Option(
        False,
        "--block-external-network",
        help="禁止 Aicd 内置工具访问非回环外网（LLM 除外）；等同 AICD_BLOCK_EXTERNAL_NETWORK=1。与子命令连写：aicd --block-external-network tui",
    ),
) -> None:
    """根命令：无子命令时，若带 --gui / --ai / --noui / … 则进入对应流程。"""
    apply_cli_block_external_network_flag(block_external_network)
    if ctx.invoked_subcommand is not None:
        return
    use_console = console_mode_requested_explicit(console)
    if gui and noui:
        typer.echo("错误: --gui 与 --noui 不能同时使用", err=True)
        raise typer.Exit(code=2)
    if gui and use_console:
        typer.echo("错误: --gui 与 --console 不能同时使用", err=True)
        raise typer.Exit(code=2)
    if gui:
        _run_gui_entry(
            home=home,
            work=work,
            out=out,
            ai=ai,
            skip_setup=skip_setup,
            no_plugins=no_plugins,
            plugin_policy=plugin_policy,
        )
        return
    if not (
        (ai and str(ai).strip())
        or noui
        or work is not None
        or out is not None
        or home is not None
        or skip_setup
        or use_console
    ):
        typer.echo(ctx.get_help())
        raise typer.Exit(code=0)
    _run_tui_entry(
        home=home,
        work=work,
        out=out,
        ai=ai,
        noui=noui,
        skip_setup=skip_setup,
        console=console,
        no_plugins=no_plugins,
        plugin_policy=plugin_policy,
    )


@app.command()
def version() -> None:
    """打印当前版本号。"""
    typer.echo(VERSION)


@app.command()
def init(
    home: Optional[Path] = typer.Option(
        None,
        "--home",
        "-h",
        help="HOME 根目录；不设则用环境变量 AICD_HOME，否则在当前用户主目录下创建并使用 .aicd",
    ),
    wizard: bool = typer.Option(
        True,
        "--wizard/--no-wizard",
        help="是否在终端交互配置 LLM（首次/无完成标记时）；非交互环境自动跳过",
    ),
) -> None:
    """初始化 HOME 目录、config.yaml 与 SQLite。"""
    root, layout, cfg = init_home(home)
    merge_env_api_key(cfg)
    merge_env_integration_tokens(cfg)
    cfg_path = layout["config"]
    if (
        wizard
        and should_prompt_setup()
        and not skip_first_time_setup_interactive(layout, cfg_path, cfg)
    ):
        cfg = run_first_time_setup(layout, cfg, home_label=str(root.resolve()))
        merge_env_api_key(cfg)
        merge_env_integration_tokens(cfg)
    else:
        persist_config(layout, cfg)
    refresh_runtime_env_file(layout)
    typer.echo(f"初始化完成: {root}")


@app.command()
def tui(
    home: Optional[Path] = typer.Option(
        None,
        "--home",
        help="HOME 根目录；不设则用 AICD_HOME，否则在用户主目录下自动创建 .aicd",
    ),
    work: Optional[Path] = typer.Option(
        None,
        "--work",
        "-w",
        help="启动后将 AI 工作区设为该目录（等同 TUI /work）；须为已存在的目录",
    ),
    out: Optional[Path] = typer.Option(
        None,
        "--out",
        "-o",
        help="AI 输出根目录（等同 /doc，内含 tmp/）；相对路径相对当前 AI 工作区",
    ),
    ai: Optional[str] = typer.Option(
        None,
        "--ai",
        help="非交互：启动后自动向主 AI 发送该任务（纯 CLI 批处理时常用）；不设则为交互模式",
    ),
    noui: bool = typer.Option(
        False,
        "--noui",
        help="不启动 TUI：仅 stdout 打印工具行与最终助手回复，任务结束后退出（须配合 --ai）",
    ),
    skip_setup: bool = typer.Option(
        False,
        "--skip-setup",
        help="跳过首次 HOME 的 LLM 交互配置（也可用环境变量 AICD_SKIP_SETUP=1）",
    ),
    console: bool = typer.Option(
        False,
        "--console",
        help="传统纯文本多轮对话（无 Textual 全屏；Win10 经典 cmd 可优先使用）；可设 AICD_CONSOLE=1",
    ),
    no_plugins: bool = typer.Option(
        False,
        "--no-plugins",
        help="启动时不加载任何 HOME 插件",
    ),
    plugin_policy: Optional[str] = typer.Option(
        None,
        "--plugin-policy",
        help="覆盖 AICD_PLUGIN_POLICY（如 off、explicit、full）",
    ),
    block_external_network: bool = typer.Option(
        False,
        "--block-external-network",
        help="禁止 Aicd 内置工具访问非回环外网（LLM 除外）；等同 AICD_BLOCK_EXTERNAL_NETWORK=1",
    ),
) -> None:
    """启动 Textual 界面（左聊天、右任务），或使用 --ai / --noui 走纯命令行。

    首次使用且无 API 密钥时，启动前会在**当前终端**走交互式 LLM 配置（与 ``aicd init`` 相同逻辑）；
    若环境无法交互，请运行 ``aicd init`` 或配置密钥后再启动。

    **--console**：不启动 Textual，使用仅 stdin/stdout 的多轮对话（无框线/备用屏）。

    也可直接用 ``aicd --ai \"…\"``（省略 ``tui`` 子命令），与本子命令等价。"""

    apply_cli_block_external_network_flag(block_external_network)
    _run_tui_entry(
        home=home,
        work=work,
        out=out,
        ai=ai,
        noui=noui,
        skip_setup=skip_setup,
        console=console,
        no_plugins=no_plugins,
        plugin_policy=plugin_policy,
    )


@app.command()
def gui(
    home: Optional[Path] = typer.Option(
        None,
        "--home",
        help="HOME 根目录；不设则用 AICD_HOME",
    ),
    work: Optional[Path] = typer.Option(
        None,
        "--work",
        "-w",
        help="启动时设置 AI 工作区（须为已存在的目录）",
    ),
    out: Optional[Path] = typer.Option(
        None,
        "--out",
        "-o",
        help="AI 输出根目录（等同 /doc）",
    ),
    ai: Optional[str] = typer.Option(
        None,
        "--ai",
        help="启动后自动发送的首条消息（可选）",
    ),
    skip_setup: bool = typer.Option(
        False,
        "--skip-setup",
        help="跳过首次 LLM 交互配置向导",
    ),
    no_plugins: bool = typer.Option(
        False,
        "--no-plugins",
        help="启动时不加载任何 HOME 插件",
    ),
    plugin_policy: Optional[str] = typer.Option(
        None,
        "--plugin-policy",
        help="覆盖 AICD_PLUGIN_POLICY",
    ),
    block_external_network: bool = typer.Option(
        False,
        "--block-external-network",
        help="禁止 Aicd 内置工具访问非回环外网（LLM 除外）；等同 AICD_BLOCK_EXTERNAL_NETWORK=1",
    ),
) -> None:
    """启动 Tk 图形窗口（Win/Linux，依赖 Python 自带 tkinter；Linux 需 python3-tk）。"""

    apply_cli_block_external_network_flag(block_external_network)
    _run_gui_entry(
        home=home,
        work=work,
        out=out,
        ai=ai,
        skip_setup=skip_setup,
        no_plugins=no_plugins,
        plugin_policy=plugin_policy,
    )


@app.command("dump-capabilities")
def dump_capabilities_cmd(
    home: Optional[Path] = typer.Option(
        None,
        "--home",
        help="HOME 根目录；不设则用 AICD_HOME",
    ),
    skip_setup: bool = typer.Option(
        True,
        "--skip-setup",
        help="跳过首次 LLM 向导（仅导出能力清单）",
    ),
    no_plugins: bool = typer.Option(
        False,
        "--no-plugins",
        help="与运行时一致：不加载 HOME 插件",
    ),
    plugin_policy: Optional[str] = typer.Option(
        None,
        "--plugin-policy",
        help="覆盖 AICD_PLUGIN_POLICY",
    ),
    as_json: bool = typer.Option(
        True,
        "--json/--text",
        help="--json 输出完整 JSON；--text 输出系统提示风格摘要",
    ),
    block_external_network: bool = typer.Option(
        False,
        "--block-external-network",
        help="禁止 Aicd 内置工具访问非回环外网（LLM 除外）；等同 AICD_BLOCK_EXTERNAL_NETWORK=1",
    ),
) -> None:
    """打印机器可读能力清单（内置工具元数据 + 插件摘要 + 调用上下文）。"""

    apply_cli_block_external_network_flag(block_external_network)
    from aicd.capabilities import (
        build_capabilities_payload,
        capabilities_json_text,
        format_capabilities_prompt_chunk,
    )

    async def _go() -> None:
        ctx = await build_runtime(
            home=home,
            skip_first_setup=skip_setup,
            setup_tty_relaxed=True,
            cli_no_plugins=no_plugins,
            plugin_policy_override=plugin_policy,
        )
        inv = ctx.invocation
        loaded = False
        if ctx.plugin_registry:
            loaded = any(p.get("ok") for p in ctx.plugin_registry.summarize())
        payload = build_capabilities_payload(
            plugin_registry=ctx.plugin_registry,
            invocation=inv,
            plugins_boot_loaded=loaded,
        )
        if as_json:
            typer.echo(capabilities_json_text(payload).rstrip())
        else:
            typer.echo(format_capabilities_prompt_chunk(payload).rstrip())
        await ctx.shutdown_graceful()

    asyncio.run(_go())


@app.command("chat-once")
def chat_once_cmd(
    home: Optional[Path] = typer.Option(
        None,
        "--home",
        help="HOME 根目录；不设则用 AICD_HOME",
    ),
    work: Path = typer.Option(
        ...,
        "--work",
        "-w",
        help="AI 工作区（须为已存在的目录）",
    ),
    message: Optional[str] = typer.Option(
        None,
        "--message",
        "-m",
        help="用户消息（与 --message-file 二选一）",
    ),
    message_file: Optional[Path] = typer.Option(
        None,
        "--message-file",
        help="从文件读取消息（UTF-8）",
    ),
    session_id: Optional[str] = typer.Option(
        None,
        "--session-id",
        help="主会话 id；多轮在同一工作区复用时传入相同值",
    ),
    skip_setup: bool = typer.Option(
        True,
        "--skip-setup/--no-skip-setup",
        help="是否跳过首次 LLM 交互向导",
    ),
    no_plugins: bool = typer.Option(
        False,
        "--no-plugins",
        help="不加载 HOME 插件",
    ),
    plugin_policy: Optional[str] = typer.Option(
        None,
        "--plugin-policy",
        help="覆盖 AICD_PLUGIN_POLICY",
    ),
    output_json: bool = typer.Option(
        False,
        "--output-json",
        help="stdout 只输出一行 JSON（供 Host 子进程解析）",
    ),
    block_external_network: bool = typer.Option(
        False,
        "--block-external-network",
        help="禁止 Aicd 内置工具访问非回环外网（LLM 除外）；等同 AICD_BLOCK_EXTERNAL_NETWORK=1",
    ),
    main_chat_mode: Optional[str] = typer.Option(
        None,
        "--main-chat-mode",
        help="仅本轮：default | chat | doc（与主进程 /mode、Host main_chat 语义一致）",
    ),
) -> None:
    """在指定工作区执行一轮主会话 chat 后退出（用于并行多工作区 / Host 子进程）。"""

    apply_cli_block_external_network_flag(block_external_network)
    from aicd.workspace_chat_once import chat_once_result_json, run_workspace_chat_once

    text = (message or "").strip()
    if message_file is not None:
        if text:
            typer.echo("错误: 不能同时指定 --message 与 --message-file", err=True)
            raise typer.Exit(code=2)
        text = message_file.read_text(encoding="utf-8").strip()
    if not text:
        typer.echo("错误: 请提供 --message 或 --message-file", err=True)
        raise typer.Exit(code=2)

    sid = session_id.strip() if session_id and str(session_id).strip() else None

    async def _go() -> None:
        result = await run_workspace_chat_once(
            home=home,
            workspace=work,
            user_message=text,
            session_id=sid,
            skip_first_setup=skip_setup,
            cli_no_plugins=no_plugins,
            plugin_policy_override=plugin_policy,
            main_chat_mode=(main_chat_mode.strip() if main_chat_mode else None),
        )
        if output_json:
            typer.echo(chat_once_result_json(result).rstrip("\n"))
        elif result.get("ok"):
            typer.echo(str(result.get("reply") or ""))
        else:
            typer.echo(str(result.get("error") or "failed"), err=True)
            raise typer.Exit(code=1)

    asyncio.run(_go())


@app.command("host")
def host_ipc_cmd(
    home: Optional[Path] = typer.Option(
        None,
        "--home",
        help="HOME 根目录；不设则用 AICD_HOME",
    ),
    skip_setup: bool = typer.Option(
        True,
        "--skip-setup",
        help="跳过首次 LLM 向导",
    ),
    no_plugins: bool = typer.Option(
        False,
        "--no-plugins",
        help="启动时不加载 HOME 插件（外进程可用 plugin_load 拉取）",
    ),
    plugin_policy: Optional[str] = typer.Option(
        None,
        "--plugin-policy",
        help="覆盖 AICD_PLUGIN_POLICY",
    ),
    require_nonce: bool = typer.Option(
        False,
        "--require-nonce",
        help="要求每行 JSON 请求含 auth.nonce，并与 --issue-nonce 写入的文件核对",
    ),
    issue_nonce: bool = typer.Option(
        True,
        "--issue-nonce/--no-issue-nonce",
        help="启动时写入 plugins/.state/host_nonces/ 并在 stderr 打印 AICD_HOST_NONCE=…",
    ),
    nonce_ttl: int = typer.Option(
        3600,
        "--nonce-ttl",
        help="nonce 有效秒数（≥60）",
    ),
    block_external_network: bool = typer.Option(
        False,
        "--block-external-network",
        help="禁止 Aicd 内置工具访问非回环外网（LLM 除外）；等同 AICD_BLOCK_EXTERNAL_NETWORK=1",
    ),
) -> None:
    """stdin 每行一条 JSON：method + params；stdout 每行一条 JSON 响应（Host IPC）。"""

    import os

    from aicd.host_stdio import run_host_stdio_loop

    apply_cli_block_external_network_flag(block_external_network)
    if require_nonce:
        os.environ["AICD_HOST_REQUIRE_NONCE"] = "1"
    os.environ["AICD_HOST_MODE"] = "1"
    nonce_ttl_sec = max(60, int(nonce_ttl))

    async def _go() -> None:
        ctx = await build_runtime(
            home=home,
            skip_first_setup=skip_setup,
            setup_tty_relaxed=True,
            cli_no_plugins=no_plugins,
            plugin_policy_override=plugin_policy,
        )
        if issue_nonce:
            from aicd.host_tokens import issue_host_nonce

            token = issue_host_nonce(
                ctx.layout["plugins_state"],
                ttl_sec=nonce_ttl_sec,
            )
            typer.echo(f"AICD_HOST_NONCE={token}", err=True)
        try:
            await run_host_stdio_loop(ctx)
        finally:
            await ctx.shutdown_graceful()

    asyncio.run(_go())


@app.command()
def serve(
    home: Optional[Path] = typer.Option(
        None,
        "--home",
        help="HOME 根目录（与 TUI 一致）；不设则用 AICD_HOME",
    ),
    host: str = typer.Option(
        "127.0.0.1",
        "--host",
        help="监听地址（生产环境请配 X-API-Key 或环境变量 AICD_API_KEY）",
    ),
    port: int = typer.Option(8765, "--port", help="监听端口"),
    api_key: Optional[str] = typer.Option(
        None,
        "--api-key",
        help="可选；要求请求头 X-API-Key 或 Authorization: Bearer …；也可设环境变量 AICD_API_KEY",
        envvar="AICD_API_KEY",
    ),
    runtime_api: bool = typer.Option(
        False,
        "--runtime-api",
        help="启用全功能 API：POST /v1/connections 与 …/invoke（与 aicd host 请求体一致）；插件可用其作为接入口",
    ),
    max_connections: int = typer.Option(
        16,
        "--max-connections",
        help="--runtime-api 时最多同时持有的独立 Runtime 连接数",
    ),
    require_invoke_nonce: bool = typer.Option(
        False,
        "--require-invoke-nonce",
        help="每档 invoke 须在 JSON 体内含 auth.nonce（与 aicd host --require-nonce 相同校验源）",
    ),
    skip_setup: bool = typer.Option(
        True,
        "--skip-setup",
        help="新建连接时跳过首次 LLM 向导",
    ),
    no_plugins: bool = typer.Option(
        False,
        "--no-plugins",
        help="默认新建的连接启动时不加载 HOME 插件（单次连接可在 POST body 覆盖）",
    ),
    plugin_policy: Optional[str] = typer.Option(
        None,
        "--plugin-policy",
        help="覆盖默认连接的 AICD_PLUGIN_POLICY",
    ),
    block_external_network: bool = typer.Option(
        False,
        "--block-external-network",
        help="禁止 Aicd 内置工具访问非回环外网（LLM 除外）；等同 AICD_BLOCK_EXTERNAL_NETWORK=1",
    ),
) -> None:
    """启动 HTTP API：默认可只读聊天历史；``--runtime-api`` 时支持多连接与 Host 同款 invoke。"""

    apply_cli_block_external_network_flag(block_external_network)
    try:
        from aicd.api_serve import run_serve
    except ImportError:
        typer.echo(
            "缺少依赖，请执行: pip install 'aicd[api]'",
            err=True,
        )
        raise typer.Exit(code=1) from None

    async def _go() -> None:
        await run_serve(
            home=home,
            host=host,
            port=port,
            api_key=api_key,
            runtime_api=runtime_api,
            max_connections=max_connections,
            require_invoke_nonce=require_invoke_nonce,
            skip_setup=skip_setup,
            no_plugins=no_plugins,
            plugin_policy=plugin_policy,
        )

    asyncio.run(_go())


def main() -> None:
    configure_cli_utf8()
    app()


if __name__ == "__main__":
    main()
