#!/usr/bin/env python3
"""
宠小它 知识本体校验脚本。

校验 docs/graph/ontology.jsonl 的：
1. JSONL 格式合法（每行一个 JSON 对象）
2. ID 唯一且符合命名规则（entity:/action:/event:/rule:/rel:）
3. 引用完整性：每个被引用的 ID（subject/object/inputs/outputs/appliesTo/appliesRules/emittedBy/appliesTo）都存在
4. 证据强度分布统计
5. 类型必填字段检查（Entity/Action/Event/Rule/Relation 各自的最小字段集）

用法：
    python3 docs/graph/validate_ontology.py [docs_root]

默认 docs_root = docs（相对当前工作目录）。
退出码：0 = 全部通过；1 = 有错误。
"""

import json
import os
import re
import sys
from collections import Counter

VALID_EVIDENCE = {"anchored", "documented", "implemented", "gap"}
VALID_TYPES = {"Entity", "Action", "Event", "Rule", "Relation"}
ID_PREFIX_BY_TYPE = {
    "Entity": "entity:",
    "Action": "action:",
    "Event": "event:",
    "Rule": "rule:",
    "Relation": "rel:",
}

REQUIRED_FIELDS_BY_TYPE = {
    "Entity": {"id", "type", "name", "fields", "evidence"},
    "Action": {"id", "type", "name", "entrypoints", "evidence"},
    "Event": {"id", "type", "name", "domain", "emittedBy", "evidence"},
    "Rule": {"id", "type", "name", "appliesTo", "evidence"},
    "Relation": {"id", "type", "subject", "predicate", "object", "evidence"},
}

# 引用字段：这些字段里看起来像本体 ID 的字符串（以 entity:/action:/event:/rule:/rel: 开头）必须存在
# 原始字段名（如 phone、code、token）不视为引用，跳过
REFERENCE_FIELDS = {
    "inputs", "outputs", "appliesTo", "appliesRules", "emittedBy",
    "subject", "object",
}
ID_PREFIXES = ("entity:", "action:", "event:", "rule:", "rel:")


def is_id_like(s):
    return isinstance(s, str) and s.startswith(ID_PREFIXES)


def load_jsonl(path):
    entries = []
    errors = []
    with open(path, "r", encoding="utf-8") as f:
        for lineno, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            try:
                obj = json.loads(line)
                obj["_lineno"] = lineno
                entries.append(obj)
            except json.JSONDecodeError as e:
                errors.append(f"line {lineno}: invalid JSON - {e}")
    return entries, errors


def validate_ids(entries):
    errors = []
    ids = set()
    for e in entries:
        eid = e.get("id")
        if not eid:
            errors.append(f"line {e['_lineno']}: missing id")
            continue
        if eid in ids:
            errors.append(f"line {e['_lineno']}: duplicate id {eid}")
        ids.add(eid)
        etype = e.get("type")
        expected_prefix = ID_PREFIX_BY_TYPE.get(etype)
        if expected_prefix and not eid.startswith(expected_prefix):
            errors.append(f"line {e['_lineno']}: id {eid} should start with {expected_prefix} for type {etype}")
    return ids, errors


def validate_required_fields(entries):
    errors = []
    for e in entries:
        etype = e.get("type")
        if etype not in VALID_TYPES:
            errors.append(f"line {e['_lineno']}: unknown type {etype}")
            continue
        required = REQUIRED_FIELDS_BY_TYPE.get(etype, set())
        missing = required - set(e.keys())
        if missing:
            errors.append(f"line {e['_lineno']}: {etype} {e.get('id')} missing fields {sorted(missing)}")
    return errors


def validate_references(entries, all_ids):
    errors = []
    for e in entries:
        eid = e.get("id", "?")
        for field in REFERENCE_FIELDS:
            if field not in e:
                continue
            val = e[field]
            if val is None:
                continue
            if isinstance(val, str):
                refs = [val]
            elif isinstance(val, list):
                refs = val
            else:
                errors.append(f"line {e['_lineno']}: {eid}.{field} must be string or list, got {type(val).__name__}")
                continue
            for ref in refs:
                if not ref:
                    continue
                # 只校验看起来像本体 ID 的引用；原始字段名（phone/code/token）跳过
                if not is_id_like(ref):
                    continue
                if ref not in all_ids:
                    errors.append(f"line {e['_lineno']}: {eid}.{field} references unknown id {ref}")
    return errors


def validate_evidence(entries):
    errors = []
    for e in entries:
        ev = e.get("evidence")
        if ev is None:
            errors.append(f"line {e['_lineno']}: {e.get('id')} missing evidence")
            continue
        if ev not in VALID_EVIDENCE:
            errors.append(f"line {e['_lineno']}: {e.get('id')} invalid evidence '{ev}', must be one of {VALID_EVIDENCE}")
    return errors


def print_stats(entries):
    type_counts = Counter(e.get("type", "?") for e in entries)
    evidence_counts = Counter(e.get("evidence", "?") for e in entries)
    print("\n=== 本体统计 ===")
    print(f"总条目数: {len(entries)}")
    print("\n按类型:")
    for t in sorted(type_counts):
        print(f"  {t:10s} {type_counts[t]}")
    print("\n按证据强度:")
    for ev in sorted(evidence_counts):
        print(f"  {ev:12s} {evidence_counts[ev]}")
    total = len(entries)
    if total:
        anchored = evidence_counts.get("anchored", 0)
        print(f"\nanchored 占比: {anchored}/{total} = {anchored/total*100:.1f}%")


def main():
    docs_root = sys.argv[1] if len(sys.argv) > 1 else "docs"
    jsonl_path = os.path.join(docs_root, "graph", "ontology.jsonl")
    if not os.path.exists(jsonl_path):
        print(f"ERROR: {jsonl_path} not found")
        sys.exit(1)

    entries, errors = load_jsonl(jsonl_path)
    if errors:
        print("=== JSONL 解析错误 ===")
        for err in errors:
            print(f"  {err}")
        sys.exit(1)

    all_ids, id_errors = validate_ids(entries)
    field_errors = validate_required_fields(entries)
    ref_errors = validate_references(entries, all_ids)
    ev_errors = validate_evidence(entries)

    all_errors = id_errors + field_errors + ref_errors + ev_errors
    if all_errors:
        print("=== 校验失败 ===")
        for err in all_errors:
            print(f"  {err}")
        print(f"\n共 {len(all_errors)} 个错误")
        print_stats(entries)
        sys.exit(1)

    print("=== 校验通过 ===")
    print(f"  {jsonl_path}: {len(entries)} 条目，引用完整，证据强度合法")
    print_stats(entries)
    sys.exit(0)


if __name__ == "__main__":
    main()
