#!/usr/bin/env python3
"""SFCraft 文档 · 物品贴图自动同步 (Python 3)

  1. 下载 sfcraft 仓库 textures/item 全部贴图到 assets/items
  2. 扫描 content 中 crafting 组件用到的物品 id (支持九格直填与 pattern 别名两种写法)
  3. 本地缺失的 id 从 Minecraft wiki 自动获取(自动挑选最新版本渲染)
  4. 仍找不到 -> 列出缺失物品并以退出码 1 结束

用法:
  python3 scripts/sync-items.py [-f]
"""

import argparse
import json
import re
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
ITEMS_DIR = ROOT / "assets" / "items"
CONTENT_DIR = ROOT / "content"
UA = "sfcraft-docs-sync/1.0 (https://github.com/saltedfishclub/sfcraft-docs)"

SF_OWNER = "saltedfishclub"
SF_REPO = "sfcraft"
SF_REF = "rev/26.2"
SF_DIR = "src/main/resources/assets/sfcraft/textures/item"
SF_LIST_URL = (
    f"https://api.github.com/repos/{SF_OWNER}/{SF_REPO}/contents/{SF_DIR}?ref={SF_REF}"
)
WIKI_API = "https://zh.minecraft.wiki/api.php"
WIKI_IMG = "https://zh.minecraft.wiki/images/"

SHORTCODE_RE = re.compile(r"\{\{<\s*crafting\b(.*?)\}\}", re.S)
PAIR_RE = re.compile(r'(\w+)="([^"]*)"')
SLOT_KEYS = ["a1", "a2", "a3", "b1", "b2", "b3", "c1", "c2", "c3", "out"]

CONNECTORS = {"and", "of", "the", "for", "with", "to", "from", "by", "in", "on", "at"}
ALIASES = {
    "tnt": "TNT",
    "tnt_minecart": "TNT_Minecart",
    "dragon_breath": "Dragon's_Breath",
    "slime_ball": "Slime",
    "snow_golem": "Snow_Golem",
    "ocelot": "Ocelot",
}


def http_get_json(url: str, retries: int = 2) -> dict:
    for i in range(retries + 1):
        try:
            req = urllib.request.Request(url, headers={"User-Agent": UA})
            with urllib.request.urlopen(req, timeout=30) as resp:
                return json.load(resp)
        except Exception:  # noqa: BLE001
            if i < retries:
                time.sleep(0.8)
            else:
                raise
    # 理论上不可达(retries 为负时 range 为空才可能走到), 兜底避免隐式返回 None
    raise RuntimeError(f"请求失败(无可用异常信息): {url}")


def http_download(url, target):
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    with urllib.request.urlopen(req, timeout=60) as resp, open(target, "wb") as f:
        f.write(resp.read())
    with open(target, "rb") as f:
        if f.read(4) != b"\x89PNG":
            raise ValueError("下载内容不是有效 PNG")


def add_item_id(ids, raw):
    v = raw.strip()
    if not v:
        return
    item = v.split(":", 1)[0].strip()
    if item:
        ids.add(item)


def scan_item_ids():
    """扫描 content 下所有 crafting 组件, 收集物品 id。"""
    ids: set[str] = set()
    for path in sorted(CONTENT_DIR.rglob("*.md")):
        try:
            text = path.read_text(encoding="utf-8")
        except OSError:
            continue
        for m in SHORTCODE_RE.finditer(text):
            params = dict(PAIR_RE.findall(m.group(1)))
            for k in SLOT_KEYS:
                if k in params:
                    add_item_id(ids, params[k])
            if "pattern" in params:
                pat = re.sub(r"\\n|;|\r?\n", " ", params["pattern"])
                for tok in pat.split():
                    if tok == ".":
                        continue
                    if tok in params:
                        add_item_id(ids, params[tok])
                    elif re.fullmatch(r"[a-z0-9_]+", tok):
                        ids.add(tok)
    return ids


def name_variants(item_id):
    """由物品 id 生成可能的 wiki 英文名(含特殊命名变体与别名)。"""
    words = [w for w in item_id.split("_") if w]
    title = "_".join(w[0].upper() + w[1:] for w in words)
    mixed = "_".join(w if w in CONNECTORS else w[0].upper() + w[1:] for w in words)
    variants = [title]
    if mixed != title:
        variants.append(mixed)
    if len(item_id) <= 4 and not re.search(r"[aeiou]", item_id):
        variants.append(item_id.upper())
    one_word = "".join(w[0].upper() + w[1:] for w in words)
    if one_word != title:
        variants.append(one_word)
    if item_id in ALIASES:
        variants.insert(0, ALIASES[item_id])
    seen = set()
    out = []
    for v in variants:
        if v not in seen:
            seen.add(v)
            out.append(v)
    return out


def wiki_candidates(name):
    """查询 wiki 图片列表, 返回符合 <英文名>_JE..[_BE..].png 规则的文件名。"""
    regex = re.compile(
        r"^" + re.escape(name) + r"(_\(item\))?_JE\d+(\.\d+)*(_BE\d+)?\.png$"
    )
    urls = [
        WIKI_API
        + "?action=query&list=allimages&aiprefix="
        + urllib.parse.quote(name + "_")
        + "&ailimit=500&format=json",
        WIKI_API
        + "?action=query&list=allimages&aiprefix="
        + urllib.parse.quote(name + "_JE")
        + "&ailimit=200&format=json",
    ]
    seen = set()
    all_names = []
    for url in urls:
        try:
            data = http_get_json(url)
            for item in data.get("query", {}).get("allimages", []) or []:
                nm = item["name"]
                if nm not in seen:
                    seen.add(nm)
                    all_names.append(nm)
        except Exception as e:  # noqa: BLE001
            print(f"  [警告] wiki 查询失败 ({name}): {e}", file=sys.stderr)
        time.sleep(0.25)
    return [n for n in all_names if regex.match(n)]


def select_best(name, candidates):
    """挑选最新版本: 优先 (item) 物品图标, 再比 JE / BE 版本号。"""
    regex = re.compile(
        r"^" + re.escape(name) + r"(_\(item\))?_JE(\d+)(\.\d+)*(_BE(\d+))?\.png$"
    )
    best = None
    best_key = None
    for cand in candidates:
        m = regex.match(cand)
        if not m:
            continue
        key = (
            1 if m.group(1) else 0,
            int(m.group(2)),
            int(m.group(5)) if m.group(5) else -1,
        )
        if best_key is None or key > best_key:
            best, best_key = cand, key
    return best


def main():
    ap = argparse.ArgumentParser(description="SFCraft 物品贴图同步")
    ap.add_argument("-f", "--force", action="store_true",
                    help="强制重新下载 sfcraft 自定义贴图")
    args = ap.parse_args()

    ITEMS_DIR.mkdir(parents=True, exist_ok=True)

    # 1. sfcraft 自定义贴图
    print("\n== 1/4 同步 sfcraft 自定义贴图")
    sf_count = sf_new = 0
    try:
        listing = http_get_json(SF_LIST_URL)
        for f in listing:
            target = ITEMS_DIR / f["name"]
            need = (
                args.force
                or not target.exists()
                or target.stat().st_size != int(f["size"])
            )
            if need:
                http_download(f["download_url"], target)
                sf_new += 1
            sf_count += 1
        print(f"  {sf_count} 个文件, 本次更新 {sf_new} 个")
    except Exception as e:  # noqa: BLE001
        print(f"  [警告] 无法访问 GitHub, 跳过 sfcraft 贴图同步: {e}", file=sys.stderr)

    # 2. 扫描组件物品 id
    print("\n== 2/4 扫描组件物品 id")
    ids = scan_item_ids()
    all_ids = sorted(ids)
    need = [i for i in all_ids if not (ITEMS_DIR / f"{i}.png").exists()]
    print(f"  使用 {len(all_ids)} 个物品 id, 本地已有 {len(all_ids) - len(need)} 个")

    # 3. 从 Minecraft wiki 获取缺失贴图
    print("\n== 3/4 从 Minecraft wiki 获取缺失贴图")
    failed = []
    for item_id in need:
        file = None
        for name in name_variants(item_id):
            cands = wiki_candidates(name)
            if cands:
                file = select_best(name, cands)
                break
        if file:
            try:
                http_download(WIKI_IMG + urllib.parse.quote(file), ITEMS_DIR / f"{item_id}.png")
                print(f"  ok    {item_id}  <-  {file}")
                continue
            except Exception as e:  # noqa: BLE001
                print(f"  fail  {item_id}  下载失败: {e}", file=sys.stderr)
        else:
            print(f"  fail  {item_id}  在 wiki 上未找到", file=sys.stderr)
        failed.append(item_id)

    # 4. 汇总错误
    if failed:
        print("\n[错误] 以下物品贴图无法自动获取:", file=sys.stderr)
        for i in sorted(failed):
            print(f"  - {i}", file=sys.stderr)
        print(
            "\n可能原因:\n"
            "  1. 物品 id 拼写错误(组件中填写的是 assets/items 下的文件名)\n"
            "  2. 该物品是 sfcraft 自定义物品, 但 textures/item 目录中没有同名贴图\n"
            "  3. 原版物品在 Minecraft wiki 上的命名特殊, 未被自动规则覆盖\n"
            "\n解决办法:\n"
            "  - 将贴图手动放入 assets/items/<id>.png 后重新运行\n"
            "  - 修正组件中的物品 id\n"
            "  - 若是 wiki 命名特例, 可在 sync-items.py 的 ALIASES 表中补充",
            file=sys.stderr,
        )
        sys.exit(1)

    print("\n全部贴图就绪")


if __name__ == "__main__":
    main()
