"""Tkinter GUI：主线程事件循环 + 后台线程内 asyncio 与 ``RuntimeContext``（Win/Linux，标准库）。"""

from __future__ import annotations

import asyncio
import contextlib
import queue
import threading
from pathlib import Path
from typing import Any

import typer

from aicd.runtime import RuntimeContext, build_runtime
from aicd.setup_wizard import runtime_llm_ready, should_prompt_setup
from aicd.ui.cli_headless import one_chat_turn, shutdown_ctx_after_cli
from aicd.agent.idle_pulse import MAIN_LIFECYCLE_UI_LOG_LINE
from aicd.agent.loop import CHAT_SUPPLEMENT_QUEUE_MAX
from aicd.ui.task_lifecycle_bus import bus_event_is_main_session_lifecycle


def _log_put(log_q: queue.Queue[tuple[str, Any]], msg: str) -> None:
    log_q.put(("log", msg))


def run_gui_app(
    *,
    home: Path | None,
    work: Path | None,
    out: Path | None,
    skip_setup: bool,
    bootstrap_ai: str | None,
    no_plugins: bool = False,
    plugin_policy: str | None = None,
) -> None:
    try:
        import tkinter as tk
        from tkinter import filedialog, messagebox, scrolledtext, ttk
    except ImportError as e:
        typer.echo(
            "无法加载 tkinter（Linux 通常需: sudo apt install python3-tk）。",
            err=True,
        )
        raise typer.Exit(code=1) from e

    log_q: queue.Queue[tuple[str, Any]] = queue.Queue()
    cmd_q: queue.Queue[tuple[str, Any]] = queue.Queue()
    stop = threading.Event()
    worker_exc: list[BaseException] = []
    ctx_holder: list[Any] = [None]
    poll_task_holder: list[asyncio.Task[None] | None] = [None]
    bus_task_holder: list[asyncio.Task[None] | None] = [None]

    def worker() -> None:
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        ctx: RuntimeContext | None = None
        poll_task: asyncio.Task[None] | None = None
        try:
            ctx = loop.run_until_complete(
                build_runtime(
                    home=home,
                    skip_first_setup=skip_setup,
                    setup_tty_relaxed=True,
                    cli_no_plugins=no_plugins,
                    plugin_policy_override=plugin_policy,
                )
            )
            ctx_holder[0] = ctx
            if (
                not skip_setup
                and not runtime_llm_ready(ctx.config)
                and not should_prompt_setup(relaxed=True)
            ):
                _log_put(
                    log_q,
                    "[aicd] 未检测到可交互终端向导；请先在终端执行: aicd init\n"
                    "或设置 OPENAI_API_KEY / 编辑 config.yaml 中的 llm.apiKey。",
                )
            if work is not None:
                ok, msg = ctx.set_ai_workspace(str(work))
                if not ok:
                    _log_put(log_q, f"[aicd] 工作区设置失败: {msg}")
            if out is not None:
                ok, msg = ctx.set_ai_output_dir(str(out))
                if not ok:
                    _log_put(log_q, f"[aicd] 输出目录设置失败: {msg}")

            chat_busy: list[bool] = [False]
            lifecycle_coalesce_holder: list[asyncio.Task[None] | None] = [None]

            async def _run_chat_turn(text: str, emit) -> None:
                chat_busy[0] = True
                try:
                    code, reply = await one_chat_turn(ctx, text, emit=emit)
                    if reply is not None:
                        _log_put(log_q, f"\n--- assistant ---\n{reply}\n")
                    log_q.put(("turn_done", code))
                finally:
                    chat_busy[0] = False

            async def _task_poll_loop() -> None:
                while not stop.is_set():
                    c = ctx_holder[0]
                    if c is not None:
                        try:
                            rows = await c.tasks.list_tasks(80)
                            active = frozenset({"running", "pending"})

                            def _ts(r: Any) -> str:
                                return getattr(r, "updated_at", None) or ""

                            running = [
                                r
                                for r in rows
                                if (getattr(r, "status", "") or "").lower()
                                in active
                            ]
                            done = [
                                r
                                for r in rows
                                if (getattr(r, "status", "") or "").lower()
                                not in active
                            ]
                            running.sort(key=_ts, reverse=True)
                            done.sort(key=_ts, reverse=True)
                            ser = [
                                {
                                    "id": r.id,
                                    "type": getattr(r, "type", ""),
                                    "status": getattr(r, "status", ""),
                                    "updated_at": getattr(r, "updated_at", ""),
                                }
                                for r in running + done
                            ]
                            log_q.put(("tasks", ser))
                        except Exception:
                            pass
                    await asyncio.sleep(0.65)

            async def _coalesced_lifecycle_wakeup() -> None:
                try:
                    delay = float(ctx.config.agent.lifecycle_coalesce_sec)
                    delay = max(0.0, min(3.0, delay))
                    await asyncio.sleep(delay)
                except asyncio.CancelledError:
                    return
                if stop.is_set():
                    return
                # 与 TUI 一致：不因 chat_busy / 队列非空而丢弃；否则子任务完成时主对话若仍在
                # 回答或用户已发「继续」会永远收不到 lifecycle 触发的 chat。
                from aicd.agent.idle_pulse import main_lifecycle_wakeup_user_text

                _log_put(log_q, MAIN_LIFECYCLE_UI_LOG_LINE)
                cmd_q.put(("chat", main_lifecycle_wakeup_user_text()))

            async def _bus_lifecycle_loop() -> None:
                bus_q = ctx.bus.subscribe()
                try:
                    while not stop.is_set():
                        try:
                            ev = await asyncio.wait_for(bus_q.get(), timeout=0.5)
                        except asyncio.TimeoutError:
                            continue
                        except asyncio.CancelledError:
                            break
                        if not bus_event_is_main_session_lifecycle(
                            ev, chat_session_id=ctx.agent.chat_session_id
                        ):
                            continue
                        t_old = lifecycle_coalesce_holder[0]
                        if t_old is not None and not t_old.done():
                            t_old.cancel()
                        lifecycle_coalesce_holder[0] = asyncio.create_task(
                            _coalesced_lifecycle_wakeup()
                        )
                finally:
                    ctx.bus.unsubscribe(bus_q)
                    t_old = lifecycle_coalesce_holder[0]
                    if t_old is not None and not t_old.done():
                        t_old.cancel()
                    lifecycle_coalesce_holder[0] = None

            first = (bootstrap_ai or "").strip()
            if first:

                def emit_boot(s: str) -> None:
                    _log_put(log_q, s)

                loop.run_until_complete(_run_chat_turn(first, emit_boot))

            poll_task = loop.create_task(_task_poll_loop())
            bus_task = loop.create_task(_bus_lifecycle_loop())
            poll_task_holder[0] = poll_task
            bus_task_holder[0] = bus_task

            log_q.put(("ready", True))

            while not stop.is_set():
                try:
                    kind, payload = cmd_q.get(timeout=0.15)
                except queue.Empty:
                    loop.run_until_complete(asyncio.sleep(0))
                    continue
                if kind == "shutdown":
                    break
                if kind == "set_work" and isinstance(payload, str) and ctx is not None:
                    ok, msg = ctx.set_ai_workspace(payload)
                    if ok:
                        _log_put(log_q, f"[aicd] 工作区: {ctx.workspace_cwd}")
                    else:
                        _log_put(log_q, f"[aicd] 工作区失败: {msg}")
                    log_q.put(("turn_done", 0))
                    continue
                if kind == "chat_supplement" and isinstance(payload, str) and ctx is not None:
                    depth, ok_sup = loop.run_until_complete(
                        ctx.agent.enqueue_chat_supplement(payload)
                    )
                    if ok_sup:
                        _log_put(
                            log_q,
                            f"[系统] 已记录「执行中补充」（队列约 {depth} 条，将于主对话下一轮工具前合并）",
                        )
                    elif depth >= CHAT_SUPPLEMENT_QUEUE_MAX:
                        _log_put(
                            log_q,
                            f"[系统] 「执行中补充」队列已满（上限 {CHAT_SUPPLEMENT_QUEUE_MAX} 条），本条未写入",
                        )
                    else:
                        _log_put(log_q, "[系统] 补充内容为空，已忽略")
                    continue
                if kind == "chat" and isinstance(payload, str) and ctx is not None:

                    def emit_chat(s: str) -> None:
                        _log_put(log_q, s)

                    loop.run_until_complete(_run_chat_turn(payload, emit_chat))
        except BaseException as e:
            worker_exc.append(e)
            log_q.put(("log", f"[aicd] 后台错误: {e}"))
        finally:
            for holder in (bus_task_holder, poll_task_holder):
                pt = holder[0]
                if pt is not None and not pt.done():
                    pt.cancel()
                    with contextlib.suppress(Exception):
                        loop.run_until_complete(pt)
                holder[0] = None
            if ctx is not None:
                try:
                    loop.run_until_complete(shutdown_ctx_after_cli(ctx))
                except Exception:
                    pass
            try:
                loop.close()
            except Exception:
                pass
            log_q.put(("worker_stopped", None))

    t = threading.Thread(target=worker, name="aicd-gui-async", daemon=True)
    t.start()

    root = tk.Tk()
    root.title("Aicd")
    root.minsize(720, 480)

    workspace_var = tk.StringVar(value="（启动后设置）")
    send_btn = tk.Button(root, text="发送", state=tk.DISABLED)

    main_pane = tk.PanedWindow(root, orient=tk.HORIZONTAL, sashrelief=tk.RAISED)
    main_pane.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

    left = tk.Frame(main_pane)
    right = tk.Frame(main_pane, width=280)
    main_pane.add(left, minsize=400, stretch="always")
    main_pane.add(right, minsize=240, stretch="never")

    top = tk.Frame(left)
    top.pack(fill=tk.X, padx=6, pady=4)
    tk.Label(top, text="AI workspace:").pack(side=tk.LEFT)
    tk.Label(top, textvariable=workspace_var, anchor="w").pack(
        side=tk.LEFT, fill=tk.X, expand=True, padx=6
    )

    def browse_work() -> None:
        d = filedialog.askdirectory(title="选择 AI 工作区")
        if d:
            cmd_q.put(("set_work", d))
            send_btn.config(state=tk.DISABLED)

    tk.Button(top, text="浏览…", command=browse_work).pack(side=tk.RIGHT)

    log = scrolledtext.ScrolledText(left, wrap=tk.WORD, state=tk.DISABLED, height=18)
    log.pack(fill=tk.BOTH, expand=True, padx=6, pady=4)

    bottom = tk.Frame(left)
    bottom.pack(fill=tk.BOTH, padx=6, pady=4)
    inp = tk.Text(bottom, wrap=tk.WORD, height=5)
    inp.pack(fill=tk.BOTH, expand=True)
    tk.Label(
        bottom,
        text="Enter 发送 · Shift+Enter 换行",
        fg="gray",
        font=("TkDefaultFont", 8),
    ).pack(anchor=tk.W)

    tk.Label(right, text="子任务", font=("TkDefaultFont", 10, "bold")).pack(
        anchor=tk.W, padx=6, pady=(4, 2)
    )
    task_tv_frame = tk.Frame(right)
    task_tv_frame.pack(fill=tk.BOTH, expand=True, padx=6, pady=2)
    task_scroll = tk.Scrollbar(task_tv_frame)
    task_scroll.pack(side=tk.RIGHT, fill=tk.Y)
    task_tree = ttk.Treeview(
        task_tv_frame,
        columns=("type", "status"),
        show="headings",
        height=14,
        yscrollcommand=task_scroll.set,
    )
    task_scroll.config(command=task_tree.yview)
    task_tree.heading("#0", text="id")
    task_tree.heading("type", text="type")
    task_tree.heading("status", text="status")
    task_tree.column("#0", width=108, stretch=True)
    task_tree.column("type", width=64, stretch=False)
    task_tree.column("status", width=72, stretch=False)
    task_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

    tk.Label(right, text="选中任务", font=("TkDefaultFont", 9)).pack(
        anchor=tk.W, padx=6, pady=(6, 2)
    )
    task_detail = scrolledtext.ScrolledText(
        right, wrap=tk.WORD, state=tk.DISABLED, height=8, font=("TkFixedFont", 9)
    )
    task_detail.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 6))

    def append_log(s: str) -> None:
        log.config(state=tk.NORMAL)
        log.insert(tk.END, s + "\n")
        log.see(tk.END)
        log.config(state=tk.DISABLED)

    def refresh_task_tree(rows: list[dict[str, Any]]) -> None:
        for item in task_tree.get_children():
            task_tree.delete(item)
        for r in rows:
            tid = str(r.get("id", ""))
            short = tid[:10] + "…" if len(tid) > 12 else tid
            task_tree.insert(
                "",
                tk.END,
                text=short,
                values=(str(r.get("type", "")), str(r.get("status", ""))),
                iid=tid,
            )

    def on_task_select(_event: Any = None) -> None:
        sel = task_tree.selection()
        task_detail.config(state=tk.NORMAL)
        task_detail.delete("1.0", tk.END)
        if not sel:
            task_detail.config(state=tk.DISABLED)
            return
        tid = sel[0]
        row = next(
            (r for r in last_task_rows if str(r.get("id")) == tid),
            None,
        )
        if row:
            line = (
                f"id: {row.get('id')}\ntype: {row.get('type')}\n"
                f"status: {row.get('status')}\nupdated: {row.get('updated_at', '')}"
            )
        else:
            line = f"id: {tid}"
        task_detail.insert(tk.END, line)
        task_detail.config(state=tk.DISABLED)

    last_task_rows: list[dict[str, Any]] = []

    def set_task_rows(rows: list[dict[str, Any]]) -> None:
        nonlocal last_task_rows
        last_task_rows = rows
        refresh_task_tree(rows)
        on_task_select()

    task_tree.bind("<<TreeviewSelect>>", on_task_select)

    def on_turn_done() -> None:
        send_btn.config(state=tk.NORMAL)
        try:
            c = ctx_holder[0]
            if c is not None:
                workspace_var.set(str(c.workspace_cwd))
        except Exception:
            pass

    def send_message() -> None:
        raw = inp.get("1.0", "end-1c")
        if not raw.strip():
            return
        append_log(f"--- you ---\n{raw}\n")
        inp.delete("1.0", tk.END)
        if chat_busy[0]:
            cmd_q.put(("chat_supplement", raw))
            return
        send_btn.config(state=tk.DISABLED)
        cmd_q.put(("chat", raw))

    def on_input_return(event: Any) -> str | None:
        send_message()
        return "break"

    def on_input_shift_return(event: Any) -> str | None:
        inp.insert(tk.INSERT, "\n")
        return "break"

    inp.bind("<Return>", on_input_return)
    inp.bind("<Shift-Return>", on_input_shift_return)

    send_btn.config(command=send_message)
    send_btn.pack(fill=tk.X, padx=6, pady=(0, 6))

    err_shown = [False]

    def poll_log() -> None:
        try:
            while True:
                typ, data = log_q.get_nowait()
                if typ == "log":
                    append_log(str(data))
                elif typ == "ready":
                    send_btn.config(state=tk.NORMAL)
                elif typ == "turn_done":
                    on_turn_done()
                elif typ == "tasks":
                    if isinstance(data, list):
                        set_task_rows(data)
                elif typ == "worker_stopped":
                    pass
        except queue.Empty:
            pass
        if worker_exc and not err_shown[0]:
            err_shown[0] = True
            try:
                messagebox.showerror("Aicd", str(worker_exc[0]))
            except Exception:
                pass
        root.after(80, poll_log)

    def on_close() -> None:
        stop.set()
        cmd_q.put(("shutdown", None))
        root.destroy()

    root.protocol("WM_DELETE_WINDOW", on_close)
    root.after(100, poll_log)
    root.mainloop()
