"""T6 创作工具箱 · 声明式生成器描述符类型（创作工具箱通用框架的契约）。

竞品把创作辅助做成一排「XX 生成器」卡片；本仓的 `AgentSpec`（ARCH §5.1）本就是
「内置 Agent 与用户 Skill 同构声明」。这里再加一层**生成器描述符**：把「一个生成器」
声明成一条 `GeneratorTool`——复用 §5.1 的 `spec`（tier/system_prompt/reads/writes），
再补前端渲染所需的卡片文案 + 表单字段 + 注入策略 + 可选入库目标。

之后「加一个生成器」= 加一份 `GeneratorTool` 声明（descriptor 在代码、不进 DB——
保 Pydantic schema 为真类，避免迁移；DB `skills` 表与只读注册表/权限故事不变）。

本模块只定义**类型**（Wave-0 契约）；`TOOLBOX: dict[str, GeneratorTool]` 注册表 seeding
属 Wave A（需 @llm 落地的 8 个新 spec/output_schema）。

不可变：所有描述符 frozen——构造后不得改动（全局 immutability 约定）。
"""

from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, ConfigDict, model_validator
from ww_agents import AgentSpec
from ww_shared import AppError, ErrorCode

from ww_skills.skill_permissions import KNOWN_TABLES

# 注入策略：决定为该生成器组哪些材料喂给网关（覆盖全部生成器）。
# - brief_only：项目立意 + 用户一句话（脑洞/书名）；
# - with_project：+ 主线/卖点（简介）；
# - with_world：+ world_entities 卡（名字/金手指/词条——须契合世界观硬规则）；
# - with_outline_chapter：+ 指定章 outline beats（细纲/黄金开篇）；
# - with_prior_chapter：+ 最新已写正文（续写——端点先读最新 accepted/draft 正文注入）；
# - text_input：+ 作者提供的原文（扩写/降AI率/拆书样本——经请求 text 字段注入）。
ContextStrategy = Literal[
    "brief_only",
    "with_project",
    "with_world",
    "with_outline_chapter",
    "with_prior_chapter",
    "text_input",
]


class InputField(BaseModel):
    """声明式表单字段描述符（前端据此渲染输入控件；frozen；snake_case）。

    `type` 是前端控件类型（"text"/"textarea"/"number"/"select" 等）；`default` 统一以
    字符串携带（声明层不区分数值/枚举的运行期类型，前端按 `type` 解释）。
    """

    model_config = ConfigDict(frozen=True)

    name: str  # 表单字段名（提交时的 key）
    label: str  # 展示标签
    type: str  # 控件类型：text / textarea / number / select ...
    required: bool = True
    default: str | None = None
    help: str | None = None  # 字段说明/占位提示


class IngestSpec(BaseModel):
    """可选入库目标描述符（None 表示纯预览不写库；frozen）。

    `table` 必须在 `KNOWN_TABLES` 创作表白名单内——驱动既有入库 gate
    （continuity 预检 + `partition_writes` 白名单 + schema→JSONB 形变，不变量 #3）。
    越权/不存在的入库表 → AppError(VALIDATION)（不可信声明的第一道闸）。
    KISS：当前只需 `table` 即可驱动 gate，更多形变字段属 Wave A 按需再加。
    """

    model_config = ConfigDict(frozen=True)

    table: str

    @model_validator(mode="after")
    def _check_table(self) -> IngestSpec:
        if self.table not in KNOWN_TABLES:
            raise AppError(
                ErrorCode.VALIDATION,
                f"IngestSpec 声明了未知入库表：{self.table}",
                {"table": self.table},
            )
        return self


class GeneratorTool(BaseModel):
    """单个生成器的声明式描述符（创作工具箱卡片 + 执行路径的真源；frozen）。

    两类形态：
    - 新工具：带 `spec`（§5.1 声明）+ `output_schema`（结构化产出真类），走通用执行器；
    - legacy 工具（已上架的 世界观/人设/大纲）：`spec=None`，带 `legacy_route` 指向
      现有更丰富的入库/冲突页面，不回归既测代码。

    `ingest=None` 表示纯预览（不写库）；`genre` 标注题材适用（前端徽标/过滤）。
    """

    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

    key: str  # 路由用稳定标识（"brainstorm" / "book-title" ...）
    title: str  # 卡片标题（"脑洞生成器"）
    subtitle: str  # 卡片副标题（"突破想象，脑洞大开"）
    spec: AgentSpec | None  # §5.1 声明（tier/system_prompt/reads/writes）；legacy 为 None
    output_schema: type[BaseModel] | None  # 结构化产出真类；legacy/纯文本可为 None
    context_strategy: ContextStrategy  # 注入策略（组哪些材料）
    input_fields: list[InputField]  # 声明式表单
    ingest: IngestSpec | None = None  # 可入库目标；None=纯预览
    legacy_route: str | None = None  # legacy 工具指向的现有页面/端点
    genre: str | None = None  # 题材适用（卡片徽标/过滤）
