from __future__ import annotations

import asyncio
import uuid
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import aiosqlite

from aicd.agent.llm import OpenAICompatibleClient, create_llm_client
from aicd.agent.loop import AgentLoop, merge_env_api_key
from aicd.agent.tool_router import ToolRouter
from aicd.bus.event_bus import AsyncEventBus
from aicd.config import AppConfig, load_config, save_config
from aicd.config_settings import (
    apply_nested_settings_patch,
    format_settings_summary,
    set_agent_scalar_from_slash,
    set_llm_scalar_from_slash,
)
from aicd.db.store import TaskStore
from aicd.env_probe import format_env_capabilities_for_prompt, refresh_runtime_env_file
from aicd.home import init_home, new_chat_session_id
from aicd.setup_wizard import (
    run_first_time_setup,
    should_prompt_setup,
    skip_first_time_setup_interactive,
)
from aicd.session_log import (
    SessionLogger,
    app_config_for_log,
    build_startup_info,
    new_session_log_path,
)
from aicd.tasks.manager import TaskManager
from aicd.tools.path_policy import PathPolicy
from aicd.tools.process_tool import ProcessTool


@dataclass
class RuntimeContext:
    home: Path
    layout: dict[str, Path]
    config: AppConfig
    bus: AsyncEventBus
    conn: aiosqlite.Connection
    store: TaskStore
    tasks: TaskManager
    router: ToolRouter
    llm: OpenAICompatibleClient
    agent: AgentLoop
    session_log: SessionLogger
    runtime_env_snapshot: dict[str, Any] = field(default_factory=dict)
    runtime_env_yaml: Path | None = None
    workspace_cwd: Path = field(init=False)
    ai_output_dir: Path = field(init=False)
    ai_tmp_dir: Path = field(init=False)
    _ai_output_user_set: bool = field(init=False, repr=False)
    _shutdown_done: bool = field(default=False, init=False, repr=False)

    def __post_init__(self) -> None:
        self.workspace_cwd = self.router.cwd
        self._ai_output_user_set = False
        self._recompute_ai_output_paths()
        self._sync_ai_paths_to_router_and_agent()

    async def shutdown_graceful(self) -> None:
        """取消后台任务并关闭数据库（可重复调用，幂等）。"""
        if self._shutdown_done:
            return
        self._shutdown_done = True
        self.session_log.write_event(
            "runtime",
            "RuntimeContext",
            "shutdown_graceful",
            "",
            "cancel_tasks_close_db",
        )
        try:
            await asyncio.wait_for(
                self.tasks.cancel_all_running(reason="safe_exit"),
                timeout=50.0,
            )
        except asyncio.TimeoutError:
            leaked = self.tasks.abandon_stale_running_tasks()
            self.session_log.write_event(
                "runtime",
                "RuntimeContext",
                "cancel_all_running_timeout",
                f"abandoned_n={len(leaked)}",
                ",".join(leaked[:12]),
            )
        except Exception as e:  # noqa: BLE001
            self.session_log.write_exception(
                "runtime",
                "RuntimeContext",
                "cancel_all_running",
                e,
            )
        try:
            await asyncio.wait_for(self.conn.close(), timeout=8.0)
        except asyncio.TimeoutError:
            self.session_log.write_event(
                "runtime",
                "RuntimeContext",
                "conn_close_timeout",
                "",
                "",
            )
        except Exception as e:  # noqa: BLE001
            self.session_log.write_exception(
                "runtime",
                "RuntimeContext",
                "conn_close",
                e,
            )

    def _recompute_ai_output_paths(self) -> None:
        """默认：工作目录下的 aicd/ 与 aicd/tmp/。"""
        self.ai_output_dir = (self.workspace_cwd / "aicd").resolve()
        self.ai_tmp_dir = (self.ai_output_dir / "tmp").resolve()

    def _sync_ai_paths_to_router_and_agent(self) -> None:
        self.router.set_ai_paths(self.ai_output_dir, self.ai_tmp_dir)
        self.agent.set_ai_storage_paths(self.ai_output_dir, self.ai_tmp_dir)

    def ensure_ai_output_layout(self) -> None:
        """首次使用或 /doc 时创建输出目录与 tmp。"""
        self.ai_output_dir.mkdir(parents=True, exist_ok=True)
        self.ai_tmp_dir.mkdir(parents=True, exist_ok=True)

    def get_ai_storage_paths(self) -> dict[str, str]:
        return {
            "workspace": str(self.workspace_cwd),
            "ai_output_dir": str(self.ai_output_dir),
            "ai_tmp_dir": str(self.ai_tmp_dir),
        }

    def set_ai_workspace(self, path_str: str | None) -> tuple[bool, str]:
        """设置主 AI 工作目录（与 AICD_HOME 无关）。无 path 时返回当前目录。"""
        if path_str is None or not str(path_str).strip():
            return True, str(self.workspace_cwd)
        raw = str(path_str).strip()
        p = Path(raw).expanduser()
        if not p.is_absolute():
            p = (self.workspace_cwd / p).resolve()
        else:
            p = p.resolve()
        try:
            self.router.path_policy.check_allowed(p)
        except PermissionError as e:
            return False, str(e)
        if not p.is_dir():
            return False, f"not a directory: {p}"
        old_ws = self.workspace_cwd.resolve()
        self.router.set_cwd(p)
        self.tasks.set_process_tool(ProcessTool(self.router.path_policy, p))
        self.workspace_cwd = p
        self.agent.set_workspace(p)
        if not self._ai_output_user_set:
            self._recompute_ai_output_paths()
        else:
            # 若用户曾设过 /doc，但目录仍在「旧工作区」下，切换 /work 后应跟到新工作区，否则会一直写到启动目录或旧项目路径
            try:
                self.ai_output_dir.resolve().relative_to(old_ws)
            except ValueError:
                pass
            else:
                self._ai_output_user_set = False
                self._recompute_ai_output_paths()
        self.ensure_ai_output_layout()
        self._sync_ai_paths_to_router_and_agent()
        return True, str(p)

    def set_ai_output_dir(self, path_str: str | None) -> tuple[bool, str]:
        """设置 AI 输出根目录（其下自动使用 tmp/）。无参数时返回当前路径；default/reset 恢复为 workspace/aicd。"""
        if path_str is None or not str(path_str).strip():
            self.ensure_ai_output_layout()
            return True, (
                f"ai_output_dir={self.ai_output_dir}\n"
                f"ai_tmp_dir={self.ai_tmp_dir}"
            )
        raw = str(path_str).strip()
        low = raw.lower()
        if low in ("default", "reset"):
            self._ai_output_user_set = False
            self._recompute_ai_output_paths()
            self.ensure_ai_output_layout()
            self._sync_ai_paths_to_router_and_agent()
            return True, f"已恢复默认（相对当前工作目录）:\n{self.ai_output_dir}"
        policy = PathPolicy(self.config.paths)
        p = Path(raw).expanduser()
        if not p.is_absolute():
            p = (self.workspace_cwd / p).resolve()
        else:
            p = p.resolve()
        try:
            policy.check_allowed(p)
        except PermissionError as e:
            return False, str(e)
        if p.exists() and not p.is_dir():
            return False, f"not a directory: {p}"
        p.mkdir(parents=True, exist_ok=True)
        self.ai_output_dir = p
        self.ai_tmp_dir = (p / "tmp").resolve()
        self._ai_output_user_set = True
        self.ensure_ai_output_layout()
        self._sync_ai_paths_to_router_and_agent()
        return True, (
            f"ai_output_dir={self.ai_output_dir}\n"
            f"ai_tmp_dir={self.ai_tmp_dir}"
        )

    def settings_summary_text(self) -> str:
        return format_settings_summary(self.config)

    def persist_config(self) -> None:
        save_config(self.layout["config"], self.config)

    def apply_settings_patch(
        self, patch: dict, *, persist: bool = True
    ) -> tuple[bool, str]:
        ok, msg = apply_nested_settings_patch(self.config, patch)
        if ok and persist:
            self.persist_config()
        return ok, msg

    def apply_slash_llm(self, args: list[str]) -> tuple[bool, str]:
        if not args:
            return True, self.settings_summary_text()
        if len(args) < 2:
            return False, "用法: /llm <键> <值>  或单独 /llm 查看当前"
        key, *rest = args
        value = " ".join(rest)
        ok, msg = set_llm_scalar_from_slash(self.config.llm, key.lower(), value)
        if ok:
            self.persist_config()
        return ok, msg

    def apply_slash_agent(self, args: list[str]) -> tuple[bool, str]:
        if not args:
            return True, self.settings_summary_text()
        if len(args) < 2:
            return (
                False,
                "用法: /agent rounds <n> | /agent subRounds <n> | /agent heartbeat <秒> | "
                "/agent subHeartbeat <秒>  或 /agent 查看（0=关闭心跳）",
            )
        key, *rest = args
        value = " ".join(rest)
        ok, msg = set_agent_scalar_from_slash(self.config, key, value)
        if ok:
            self.persist_config()
        return ok, msg


async def build_runtime(
    home: Path | None = None,
    session_id: str | None = None,
    *,
    skip_first_setup: bool = False,
    setup_tty_relaxed: bool = False,
) -> RuntimeContext:
    root, layout, cfg = init_home(home)
    _runtime_path, _runtime_snap = refresh_runtime_env_file(layout)
    _caps_block = format_env_capabilities_for_prompt(
        _runtime_snap, yaml_path=str(_runtime_path.resolve())
    )
    merge_env_api_key(cfg)
    cfg_path = layout["config"]
    if (
        not skip_first_setup
        and should_prompt_setup(relaxed=setup_tty_relaxed)
        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)
    if not cfg.llm.api_key and cfg_path.is_file():
        cfg = load_config(cfg_path)
        merge_env_api_key(cfg)
    bus = AsyncEventBus()
    store = TaskStore(layout["db"])
    conn = await store.connect()
    policy = PathPolicy(cfg.paths)
    start_cwd = Path.cwd().resolve()
    sid = session_id or new_chat_session_id(layout["data"])
    log_path = new_session_log_path(layout["logs"])
    session_log = SessionLogger(log_path)
    session_log.write_startup(
        build_startup_info(
            home=root,
            session_log_path=log_path,
            session_id=sid,
            workspace_cwd=start_cwd,
            cfg_summary=app_config_for_log(cfg),
            layout=layout,
            runtime_env_path=str(_runtime_path),
        )
    )
    process = ProcessTool(policy, start_cwd)
    task_mgr = TaskManager(
        store=store,
        conn=conn,
        bus=bus,
        tasks_root=layout["tasks"],
        process_tool=process,
        session_log=session_log,
    )
    router = ToolRouter(
        cfg,
        layout=layout,
        task_manager=task_mgr,
        cwd=start_cwd,
        path_policy=policy,
        session_log=session_log,
        chat_session_id=sid,
    )
    llm = create_llm_client(cfg)
    llm.attach_session_log(session_log)
    agent = AgentLoop(
        cfg,
        llm,
        router,
        task_mgr,
        store,
        conn,
        layout,
        bus,
        sid,
        session_log=session_log,
        env_capabilities_block=_caps_block,
    )
    router.set_env_capabilities_callback(
        lambda data, p: agent.set_env_capabilities_block(
            format_env_capabilities_for_prompt(data, yaml_path=str(p.resolve()))
        )
    )
    router.set_chat_session_switch(agent.apply_chat_session_id)
    task_mgr.set_ai_paths_resolver(router.get_ai_paths)
    task_mgr.set_ai_runner(agent.run_sub_agent_worker)
    await agent.hydrate_chat_from_store()
    ctx = RuntimeContext(
        home=root,
        layout=layout,
        config=cfg,
        bus=bus,
        conn=conn,
        store=store,
        tasks=task_mgr,
        router=router,
        llm=llm,
        agent=agent,
        session_log=session_log,
        runtime_env_snapshot=_runtime_snap,
        runtime_env_yaml=_runtime_path,
    )
    router.set_ai_workspace_setter(ctx.set_ai_workspace)
    router.set_vision_llm(llm)
    return ctx


def persist_config(layout: dict[str, Path], cfg: AppConfig) -> None:
    save_config(layout["config"], cfg)
