#!/usr/bin/env python3
"""校验创作知识 ingest payload —— 结构 + How/What/Why 一致性。

接受单个 payload 对象或 payload 数组（一帖多颗）。报 ERROR（必须修）/ WARN（建议看）。
用法：
    python 创作知识提取-skill/tools/lint-payload.py outputs/<帖>/payload.json
"""
from __future__ import annotations

import json
import sys
from pathlib import Path

SCOPE_TYPES = {"substance", "form", "feeling", "effect", "intent"}
EXT_KEYS = {"业务阶段", "创作阶段", "动作", "出自", "概要"}
KINDS = {"子集", "多维关系", "序列"}
业务阶段 = {"灵感", "选题", "脚本"}
创作阶段 = {"定向", "构思", "结构", "成文", "打磨"}


def lint_one(p: dict, idx: int) -> tuple[list[str], list[str]]:
    err: list[str] = []
    warn: list[str] = []
    tag = f"[#{idx} {p.get('title', '?')}]"

    # 必填
    for f in ("source", "title", "content", "dim_creations", "dim_attributes", "scopes"):
        if not p.get(f):
            err.append(f"{tag} 缺必填字段 {f}")
    src = p.get("source") or {}
    for f in ("id", "source_type", "title"):
        if not src.get(f):
            err.append(f"{tag} source.{f} 缺失")

    # dim_creations 写死
    if p.get("dim_creations") != ["创作"]:
        err.append(f"{tag} dim_creations 必须恒为 [\"创作\"]，实为 {p.get('dim_creations')}")

    # dim_attributes 单值 + 类型前缀
    da = p.get("dim_attributes") or []
    if len(da) != 1:
        err.append(f"{tag} dim_attributes 应恰好 1 个，实为 {da}")
    ktype = (da[0][:3] if da else "")  # how / wha / why
    kind = {"how": "how", "wha": "what", "why": "why"}.get(ktype)
    if kind is None:
        err.append(f"{tag} dim_attributes[0] 须以 how/what/why 开头，实为 {da}")

    # scopes
    for s in p.get("scopes") or []:
        if s.get("scope_type") not in SCOPE_TYPES:
            err.append(f"{tag} scope_type 非法：{s.get('scope_type')}")
        if not s.get("value"):
            err.append(f"{tag} scope 缺 value（须带值）")

    # custom_ext
    ext = p.get("custom_ext") or []
    keys_present = set()
    for e in ext:
        k = e.get("key")
        keys_present.add(k)
        if k not in EXT_KEYS:
            err.append(f"{tag} custom_ext.key 非法：{k}")
        if e.get("type") != "str":
            err.append(f"{tag} custom_ext.type 须为 str，实为 {e.get('type')}")
        v = e.get("value")
        if not v:
            err.append(f"{tag} custom_ext[{k}] 缺 value")
        elif k == "业务阶段" and v not in 业务阶段:
            err.append(f"{tag} 业务阶段 非法值：{v}（应 ∈ {sorted(业务阶段)}）")
        elif k == "创作阶段" and v not in 创作阶段:
            err.append(f"{tag} 创作阶段 非法值：{v}（应 ∈ {sorted(创作阶段)}）")

    # content 一律是 string（ingest 接口要求）；what/why 是结构化对象的 json.dumps，解析回 dict 再校验
    raw = p.get("content")
    if not isinstance(raw, str):
        err.append(f"{tag} content 须为 string（接口要求），实为 {type(raw).__name__}")
    parsed = None
    if isinstance(raw, str) and kind in ("what", "why"):
        try:
            parsed = json.loads(raw)
        except (ValueError, TypeError):
            err.append(f"{tag} {kind} 的 content 应是结构化对象的 JSON 字符串，解析失败")

    # How vs What/Why 形态一致性
    if kind == "how":
        if "创作阶段" not in keys_present:
            warn.append(f"{tag} how 颗通常每步带『创作阶段』，custom_ext 里没有")
        if "动作" not in keys_present:
            warn.append(f"{tag} how 颗通常带『动作』（从内容提炼），custom_ext 里没有")
        c = raw if isinstance(raw, str) else ""
        if "步骤" not in c and "目标" not in c:
            warn.append(f"{tag} how 的 content 建议含『目标…』+『步骤N…』结构")
    elif kind in ("what", "why"):
        bad = keys_present & {"创作阶段", "动作"}
        if bad:
            err.append(f"{tag} {kind} 简化版不应有 {bad}（只保留业务阶段）")
        if kind == "what":  # What 的 content 解析后是结构化实体对象
            c = parsed
            if not isinstance(c, dict):
                err.append(f"{tag} what 的 content 解析后须为对象 {{name,kind,body}}，实为 {type(c).__name__}")
            else:
                if not c.get("name"):
                    err.append(f"{tag} what.content 缺 name")
                if c.get("kind") not in KINDS:
                    err.append(f"{tag} what.content.kind 非法：{c.get('kind')}（应 ∈ {sorted(KINDS)}）")
                if not c.get("body"):
                    err.append(f"{tag} what.content.body 为空")
                for j, it in enumerate(c.get("body") or [], 1):
                    if not it.get("item_name"):
                        err.append(f"{tag} body[{j}] 缺 item_name")
                if c.get("kind") == "子集" and not c.get("维度拆分规则"):
                    warn.append(f"{tag} 子集型建议填 维度拆分规则")
            if "概要" not in keys_present:
                warn.append(f"{tag} what 颗建议 custom_ext 带『概要』")
        if kind == "why":  # Why 的 content 解析后是结构化对象 {name,desc}
            c = parsed
            if not isinstance(c, dict):
                err.append(f"{tag} why 的 content 解析后须为对象 {{name,desc}}，实为 {type(c).__name__}")
            else:
                if not c.get("name"):
                    err.append(f"{tag} why.content 缺 name")
                if not c.get("desc"):
                    err.append(f"{tag} why.content 缺 desc")
    if ext and "业务阶段" not in keys_present:
        warn.append(f"{tag} custom_ext 建议至少有一个『业务阶段』")

    return err, warn


def main() -> None:
    if len(sys.argv) < 2:
        print("用法：lint-payload.py <payload.json>")
        sys.exit(2)
    data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
    payloads = data if isinstance(data, list) else [data]
    all_err, all_warn = [], []
    for i, p in enumerate(payloads, 1):
        e, w = lint_one(p, i)
        all_err += e
        all_warn += w
    for w in all_warn:
        print("WARN ", w)
    for e in all_err:
        print("ERROR", e)
    print(f"\n{len(payloads)} 颗知识：{len(all_err)} ERROR / {len(all_warn)} WARN")
    sys.exit(1 if all_err else 0)


if __name__ == "__main__":
    main()
