"""机器可读能力自述（完整工具 schema + 插件摘要 + 调用上下文 + AI 自举说明）。"""

from __future__ import annotations

import copy
import json
from typing import Any

from aicd.agent.tools_openai import openai_tools
from aicd.invocation import InvocationContext
from aicd.version import VERSION

CAPABILITIES_SCHEMA = "aicd.capabilities.v2"

# 自述里给模型看的固定说明（与 payload 合并输出）
_SELF_MODEL_FOR_PLUGIN_AUTHORS = {
    "runtime_role": (
        "主会话中的模型通过 OpenAI 兼容 API 的 function calling 调用工具；"
        "工具名必须与下方 openai_tools_merged 中的 function.name 完全一致。"
    ),
    "plugin_naming": (
        "HOME 插件在 tools.json 里声明的 function.name 为短名；"
        "加载后对外暴露为 plugin_<plugin_id>__<短名>，与内置工具隔离。"
    ),
    "handlers_keys": (
        "handlers.py 的 HANDLERS 字典键必须是 tools.json 中的原始短名（无 plugin_ 前缀）。"
    ),
    "out_of_process_bridge": (
        "独立进程（如 Web 服务、插件宿主）不应自实现 AgentLoop；应通过 aicd host（stdio 每行 JSON）"
        "或 aicd serve --runtime-api（POST /v1/connections/*/invoke，请求体与 host 一致）调用："
        "main_chat|chat_turn（可选 params.main_chat_mode=default|chat|doc 仅该轮）、"
        "main_chat_workspace/batch（同上，经子进程 aicd chat-once --main-chat-mode）、"
        "main_chat_mode_set（params.mode + persist 默认 true 写入 HOME/data/tui_prefs.json）、"
        "main_chat_supplement（同上；返回 queue_depth + enqueued，队列满时 enqueued=false）、"
        "main_inbox_continue（主收件箱有未读时自动跑一轮接续对话；亦可传 main_chat_mode）、"
        "main_chat_session_attach、workspace_set、main_chat_workspace、"
        "main_chat_workspace_batch、task_ai、capabilities、plugin_load、tool_invoke（白名单）等。"
        "多会话：HTTP 用多个 connection_id；SQLite 会话用 chat_session_id / attach。"
        "宿主若设置环境变量 **AICD_HOST_REQUIRE_NONCE=1**（或 API 层 require_nonce），"
        "每条 invoke 须在 **auth.nonce** 带 **issue_host_nonce(state_dir)** 签发的一次性令牌。"
    ),
    "full_export": (
        "终端执行：aicd dump-capabilities [--json|--text]；"
        "或向 aicd host 发送 method=capabilities / capabilities_json。"
    ),
    "docs_in_repo": (
        "仓库 docs/AICD_FEATURES.md、docs/PLUGIN_AUTHORING_SPEC.md 与实现保持同步；"
        "生成插件前应核对 PLUGIN_AUTHORING_SPEC。"
    ),
}


def _quick_index_from_merged(merged: list[dict[str, Any]]) -> list[dict[str, Any]]:
    out: list[dict[str, Any]] = []
    for item in merged:
        fn = item.get("function") if isinstance(item, dict) else None
        if not isinstance(fn, dict):
            continue
        name = fn.get("name")
        if not name:
            continue
        desc = str(fn.get("description") or "")
        out.append(
            {
                "name": str(name),
                "description_preview": desc[:500] + ("…" if len(desc) > 500 else ""),
            }
        )
    return out


def _core_only_quick_tools() -> list[dict[str, Any]]:
    tools: list[dict[str, Any]] = []
    for item in openai_tools():
        fn = item.get("function") if isinstance(item, dict) else None
        if not isinstance(fn, dict):
            continue
        name = fn.get("name")
        if not name:
            continue
        tools.append(
            {
                "name": str(name),
                "description": str(fn.get("description") or "")[:4000],
            }
        )
    return tools


def _plugin_prefixed_names(merged: list[dict[str, Any]]) -> list[str]:
    names: list[str] = []
    for item in merged:
        fn = item.get("function") if isinstance(item, dict) else None
        if not isinstance(fn, dict):
            continue
        n = fn.get("name")
        if isinstance(n, str) and n.startswith("plugin_"):
            names.append(n)
    return sorted(set(names))


def build_capabilities_payload(
    *,
    plugin_registry: Any | None,
    invocation: InvocationContext | None,
    plugins_boot_loaded: bool,
) -> dict[str, Any]:
    extra: list[dict[str, Any]] | None = None
    if plugin_registry is not None:
        try:
            ex = plugin_registry.openai_extra_tools()
            if ex:
                extra = list(ex)
        except Exception:  # noqa: BLE001
            extra = None

    merged = openai_tools(extra=extra)
    # 深拷贝避免下游意外改动静态 schema
    openai_merged = copy.deepcopy(merged)

    plugins: list[dict[str, Any]] = []
    if plugin_registry is not None:
        try:
            plugins = list(plugin_registry.summarize())
        except Exception:  # noqa: BLE001
            plugins = []
    inv_d: dict[str, Any] | None = None
    if invocation is not None:
        inv_d = {
            "depth": invocation.depth,
            "spawned_by": invocation.spawned_by,
            "parent_plugin_id": invocation.parent_plugin_id,
            "plugin_policy": invocation.plugin_policy or None,
            "host_nonce_set": bool(invocation.host_nonce),
        }

    core_only = openai_tools()
    host_methods = [
        {"method": "ping", "params": {}},
        {"method": "capabilities", "params": {}},
        {"method": "capabilities_json", "params": {}},
        {
            "method": "main_chat",
            "aliases": ["chat_turn"],
            "params": {
                "message": "string (user turn)",
                "main_chat_mode": "optional: default|chat|doc — apply for this turn only",
            },
        },
        {
            "method": "main_chat_mode_set",
            "params": {
                "mode": "optional: default|chat|doc; omit to query current",
                "persist": "optional bool (default true) — write HOME/data/tui_prefs.json",
            },
        },
        {
            "method": "main_chat_supplement",
            "aliases": ["enqueue_main_chat_supplement"],
            "params": {
                "text": "string — 合并进当前主对话轮次（下一轮 LLM 前）；和 TUI 忙时发聊天气泡同语义",
                "message": "同 text 的别名",
            },
        },
        {
            "method": "main_inbox_continue",
            "params": {
                "lifecycle_only": (
                    "optional bool (default true): only when unread lifecycle exists; "
                    "字符串 false/0/no/off 视为 false"
                ),
                "message": "optional string: override synthetic user line (else 子任务终态 template)",
                "main_chat_mode": "optional: default|chat|doc — this turn only",
            },
        },
        {
            "method": "main_chat_session_attach",
            "params": {"chat_session_id": "string — 切换到已有 SQLite 会话并 hydrate"},
        },
        {
            "method": "workspace_set",
            "params": {"path": "或 workspace: 已存在目录（path_policy 允许）"},
        },
        {
            "method": "main_chat_workspace",
            "params": {
                "message": "string",
                "workspace": "optional: existing dir path",
                "allocate_workspace": "optional bool: create UUID dir under plugins/<plugin_id>/_aicd_chat_spaces/",
                "plugin_id": "required if allocate_workspace",
                "session_id": "optional string",
                "no_plugins": "optional bool for child chat-once",
                "main_chat_mode": "optional: default|chat|doc — passed to child chat-once",
            },
        },
        {
            "method": "main_chat_workspace_batch",
            "params": {
                "items": "list of main_chat_workspace param objects (max 16)",
            },
        },
        {
            "method": "task_ai",
            "params": {"instruction": "string", "topic_slug": "optional"},
        },
        {"method": "plugin_load", "params": {"scope": "all|listed", "plugin_ids": "optional list"}},
        {"method": "plugin_reload", "params": {}},
        {
            "method": "tool_invoke",
            "params": {"name": "allowlisted tool", "arguments": "object"},
        },
    ]

    return {
        "schema": CAPABILITIES_SCHEMA,
        "aicd_version": VERSION,
        "plugins_boot_loaded": plugins_boot_loaded,
        "tools": {
            "core_only_count": len(core_only),
            "merged_count": len(merged),
            "plugin_prefixed_names": _plugin_prefixed_names(merged),
        },
        "builtin_tools_core": _core_only_quick_tools(),
        "all_tools_quick_index": _quick_index_from_merged(merged),
        "openai_tools_merged": openai_merged,
        "loaded_plugins": plugins,
        "invocation": inv_d,
        "self_model_for_ai_and_plugins": dict(_SELF_MODEL_FOR_PLUGIN_AUTHORS),
        "host_ipc": {"stdio_command": "aicd host", "methods": host_methods},
        "http_serve": {
            "cli": "aicd serve [--runtime-api]（需 pip install 'aicd[api]'）",
            "auth": "可选 Header X-API-Key / Bearer（AICD_API_KEY）",
            "read_chat_history": {
                "GET /v1/sessions/{chat_session_id}/overview": (
                    "消息/摘记计数 + context_budget_tokens、chat_history_max_messages、图像预算(8K/张)与追溯链接说明"
                ),
                "GET /v1/sessions/{chat_session_id}/messages": "分页读 SQLite 主会话消息",
                "GET /v1/sessions/{chat_session_id}/summaries": "读摘记",
            },
            "runtime_multi_connection": {
                "POST /v1/connections": (
                    "body 可选 chat_session_id（延续某 SQLite 会话）、no_plugins、plugin_policy；"
                    "返回 connection_id + chat_session_id"
                ),
                "DELETE /v1/connections/{connection_id}": "关闭该 Runtime",
                "POST /v1/connections/{connection_id}/invoke": (
                    "请求体与 aicd host 单行 JSON 相同：method、params、可选 id、auth.nonce"
                ),
            },
            "session_model": (
                "connection_id：隔离内存中的 Runtime（插件加载态、AgentLoop）；"
                "chat_session_id：SQLite 中主对话主键，可在建连时指定或用 main_chat_session_attach 切换。"
            ),
            "plugin_and_ai_bridge": (
                "模型通过 function calling 使用 plugin_* 工具扩展能力（转码、桌面自动化等）；"
                "插件进程可用 HTTP invoke 或子进程 aicd host/chat-once 回调 Aicd，"
                "并配合 aicd.invocation.subprocess_env_for_child 传递 AICD_*。"
            ),
        },
        "documentation_hints": {
            "features_md": "docs/AICD_FEATURES.md",
            "plugin_spec_md": "docs/PLUGIN_AUTHORING_SPEC.md",
            "cli_dump": "aicd dump-capabilities [--json|--text]",
        },
    }


def format_capabilities_prompt_chunk(
    payload: dict[str, Any],
    *,
    max_quick_index_names: int = 60,
) -> str:
    """供系统提示使用：固定自举说明 + 当前运行摘要（完整 schema 见 dump-capabilities / payload JSON）。"""

    sm = payload.get("self_model_for_ai_and_plugins")
    if not isinstance(sm, dict):
        sm = _SELF_MODEL_FOR_PLUGIN_AUTHORS

    tinfo = payload.get("tools") or {}
    merged_n = tinfo.get("merged_count", 0)
    core_n = tinfo.get("core_only_count", 0)
    pp = tinfo.get("plugin_prefixed_names") or []
    if isinstance(pp, list):
        pp_preview = [str(x) for x in pp[:25]]
        pp_extra = len(pp) - len(pp_preview)
        pp_line = ", ".join(pp_preview)
        if pp_extra > 0:
            pp_line += f" … (+{pp_extra})"
    else:
        pp_line = ""

    qidx = payload.get("all_tools_quick_index") or []
    names = [
        str(x.get("name"))
        for x in qidx
        if isinstance(x, dict) and x.get("name")
    ]
    preview = names[:max_quick_index_names]
    extra_n = len(names) - len(preview)
    tail = f"\n- … **+{extra_n}** tools (see **openai_tools_merged** in JSON)." if extra_n > 0 else ""

    pls = payload.get("loaded_plugins") or []
    pi = len(pls) if isinstance(pls, list) else 0
    inv = payload.get("invocation")
    inv_l = ""
    if isinstance(inv, dict):
        inv_l = (
            f"\n- **invocation**: depth={inv.get('depth')}, spawned_by={inv.get('spawned_by')!r}, "
            f"plugins_boot_loaded={payload.get('plugins_boot_loaded')}"
        )

    lines = [
        "## AICD capability self-model (for tool use & plugin authoring)",
        "### How you are wired",
        f"- **aicd_version**: {payload.get('aicd_version')!r}; payload **schema**: {payload.get('schema')!r}.",
        f"- **openai_tools_merged** count: **{merged_n}** (core-only without HOME plugins: **{core_n}**).",
        "- You must call tools by **exact** names from **openai_tools_merged** / the live API tool list.",
        f"- **Runtime**: {sm.get('runtime_role', '')}",
        f"- **Plugin tool naming**: {sm.get('plugin_naming', '')}",
        f"- **handlers.py keys**: {sm.get('handlers_keys', '')}",
        "### Out-of-process / Web plugins",
        f"- {sm.get('out_of_process_bridge', '')}",
        "### Export full machine-readable catalog",
        f"- {sm.get('full_export', '')}",
        f"- {sm.get('docs_in_repo', '')}",
        "### This instance (snapshot)",
        f"- **HOME plugins rows**: {pi}.{inv_l}",
    ]
    if pp_line:
        lines.append(f"- **Plugin-prefixed tool names** (sample): {pp_line}")
    lines.extend(
        [
            f"- **Tool names preview** (merged, first {len(preview)}): {', '.join(preview)}{tail}",
            "- When **authoring** a new HOME plugin: read **docs/PLUGIN_AUTHORING_SPEC.md**; use **plugin_info** / **plugin_validate**; prefer **plugin_backup_now** before edits.",
        ]
    )
    return "\n".join(lines)


def capabilities_json_text(payload: dict[str, Any], *, indent: int = 2) -> str:
    return json.dumps(payload, ensure_ascii=False, indent=indent) + "\n"
