"""世界包(World Pack)接入规范的发现与校验 —— 多世界平台的准入闸口。

一个"世界包"= `worlds/<id>/` 目录,自描述一个可玩世界。契约见 docs/world-pack-spec.md。
平台靠本模块**发现**可玩世界、**校验**世界包是否合规(再决定能否加载),从而做到
"加新世界纯靠丢一个目录、不改引擎"(开闭原则)。

设计:校验只读数据、不构造引擎;返回问题清单(空=合规)。错误([E])阻断加载,
警告([W])可加载但提示作者。引擎运行期对缺失项一律有回退(向前兼容),故多数文件可选。
"""

from __future__ import annotations

import json
from pathlib import Path

from genesis.manifest.builder import WorldBuilder
from genesis.manifest.schema import ManifestError

# 世界包文件清单(* = 必需,其余可选,缺则引擎回退默认)
REQUIRED_DIRS = ("characters", "places")
OPTIONAL_FILES = ("acts.json", "items.json", "world.json", "viz.json", "beats.json")


def discover_worlds(root: str | Path = "worlds") -> list[str]:
    """列出 root 下所有看起来像世界包的子目录 id(含 characters/ 与 places/)。"""
    root = Path(root)
    if not root.is_dir():
        return []
    out = []
    for d in sorted(root.iterdir()):
        if d.is_dir() and (d / "characters").is_dir() and (d / "places").is_dir():
            out.append(d.name)
    return out


def _load_json(path: Path) -> tuple[dict | None, str | None]:
    if not path.exists():
        return None, None
    try:
        return json.loads(path.read_text(encoding="utf-8")), None
    except (json.JSONDecodeError, OSError) as e:
        return None, f"{path.name} 不是合法 JSON:{e}"


def validate_world(world_dir: str | Path) -> list[str]:
    """校验一个世界包是否合规。返回问题清单(空=完全合规)。
    [E]=错误(应阻断加载);[W]=警告(可加载,建议修)。"""
    world_dir = Path(world_dir)
    issues: list[str] = []
    if not world_dir.is_dir():
        return [f"[E] 世界目录不存在:{world_dir}"]
    for d in REQUIRED_DIRS:
        if not (world_dir / d).is_dir():
            issues.append(f"[E] 缺少必需目录 {d}/")
    if any(i.startswith("[E] 缺少必需目录") for i in issues):
        return issues

    # 1) 地点/人物/邻接/出生地(复用 manifest 准入)
    try:
        builder = WorldBuilder.from_directory(str(world_dir))
        builder.validate_refs()
    except ManifestError as e:
        return issues + [f"[E] manifest 校验失败:{e}"]
    place_names = {pm.name for pm in builder.places.values()}
    roster = {cm.name for cm in builder.characters.values()}
    item_ids: set[str] = set()
    if not roster:
        issues.append("[E] 没有任何角色")
    if not place_names:
        issues.append("[E] 没有任何地点")

    def _check_places(label: str, names, sev: str = "E") -> None:
        # sev=E:物品/设施所在地必须存在(否则不可达=真 bug);sev=W:引擎会与现实取交集的"建议清单"
        for n in names:
            if n and n not in place_names:
                issues.append(f"[{sev}] {label} 引用了不存在的地点「{n}」")

    # 2) items.json(道具层):地点合法、reveal_item 指向存在的物品
    items, err = _load_json(world_dir / "items.json")
    if err:
        issues.append(f"[E] {err}")
    elif items:
        for it in items.get("items", []):
            if it.get("id"):
                item_ids.add(it["id"])
        _check_places("items.json 物品", [it.get("place") for it in items.get("items", [])])
        for f in items.get("facilities", []):
            if f.get("place"):
                _check_places("items.json 设施", [f.get("place")])
        for f in items.get("facilities", []):
            for verb, spec in (f.get("verbs") or {}).items():
                ri = spec.get("reveal_item")
                if ri and ri not in item_ids:
                    issues.append(f"[W] 设施 {f.get('id')} 的动词「{verb}」reveal_item 指向未定义物品「{ri}」")

    # 3) world.json(世界层):环境语义里的地点都得存在
    wcfg, err = _load_json(world_dir / "world.json")
    if err:
        issues.append(f"[E] {err}")
    elif wcfg:
        _check_places("world.json place_cause", list((wcfg.get("place_cause") or {}).keys()), "W")
        _check_places("world.json conspicuous", wcfg.get("conspicuous") or [], "W")
        _check_places("world.json ceremonial_venues", wcfg.get("ceremonial_venues") or [], "W")
        _check_places("world.json secret_passages", wcfg.get("secret_passages") or [], "W")

    # 4) acts.json(剧情层):目标/dossier 在名册内、取景地存在
    acts, err = _load_json(world_dir / "acts.json")
    if err:
        issues.append(f"[E] {err}")
    elif acts:
        for a in acts.get("kill_acts", []):
            t = a.get("target")
            if t and t not in roster:
                issues.append(f"[E] acts.json 第{a.get('act')}幕 target「{t}」不在角色名册内")
            _check_places(f"acts.json 第{a.get('act')}幕取景地", a.get("place") or [], "W")
        for name, d in (acts.get("dossier") or {}).items():
            if name == "_doc":
                continue
            if name not in roster:
                issues.append(f"[E] acts.json dossier 角色「{name}」不在名册内")
            ep = (d or {}).get("evidence_place") if isinstance(d, dict) else None
            if ep and ep not in place_names:    # #3 议程锚:物证地点必须是真地点,否则探索目标指向虚无
                issues.append(f"[W] acts.json dossier「{name}」的 evidence_place「{ep}」不是已知地点")

    # 5) 凶手:推理类世界应恰有一名 role 含「凶手」的角色(无则警告)
    killers = [cm.name for cm in builder.characters.values() if "凶手" in (cm.role or "")]
    if not killers:
        issues.append("[W] 没有 role 含「凶手」的角色(推理类世界通常需要一名隐藏凶手)")
    elif len(killers) > 1:
        issues.append(f"[W] 有多名凶手角色 {killers}(引擎按第一名处理,确认是否有意)")

    return issues


def describe_world(world_dir: str | Path) -> dict:
    """给平台的世界列表用:返回 {id, roster, places, has_acts, valid, issues} 概要。"""
    world_dir = Path(world_dir)
    issues = validate_world(world_dir)
    info = {"id": world_dir.name, "roster": [], "places": 0, "has_acts": (world_dir / "acts.json").exists(),
            "valid": not any(i.startswith("[E]") for i in issues), "issues": issues}
    try:
        builder = WorldBuilder.from_directory(str(world_dir))
        info["roster"] = sorted(cm.name for cm in builder.characters.values())
        info["places"] = len(builder.places)
    except ManifestError:
        pass
    return info
