#!/usr/bin/env python3
"""release-check — 回頭問「這一版真的送到了嗎」，而且**問的人不是送的人**
                  （inkstone/ISEP#67）

leo 2026-08-27 的病（票上原文）：
    「版本只到 ISEP v0.3.9，你有新版為什麼不推？」
    v0.4.0／v0.5.0 的 tag 早就推了，但 Gitea Releases 頁停在 v0.3.9
    ⇒ 對 leo 等於這兩版不存在；claude plugin list 也還顯示舊版。

ISEP 的運送鏈有這幾段，**每一段都要有人回頭問「到了嗎」**：

    定版 commit  →  tag（Gitea）  →  release（Gitea）  →  main 的 plugin.json
                                                       →  這個 session 真的載入到的版本

🔴 這支的全部價值在一句話：**它不聽上一站說「我送了」，它自己去問收貨的那一端。**
   tag 在不在 → 問 Gitea 的 tags；release 在不在 → 問 Gitea 的 releases（leo 會看的那頁）；
   載入到了沒 → 看這個 session 真的掛著的那一份 plugin.json。
   送的人（release-ship）說「成功」不算數——這支拿收貨端的事實覆核。

判準是**結構**，不是關鍵字（leo 2026-08-17 已證偽關鍵字黑名單：8 次誤攔、0 次正確）：
   · 哪些版本「應該」被送到 ＝ main 上有 `定版 vX.Y.Z` commit 的那些（git 說了算，不是猜）
   · 那個版本「送到了沒」＝ 去 Gitea 問它的 tag 與 release 在不在（在＝到了）

離開碼：
    0  現行版本（plugin.json 宣稱的那一版）**tag 與 release 都在** ⇒ 送貨端完成
    2  現行版本缺 tag 或缺 release ⇒ **送貨端斷了**，stderr 講出缺哪一站＋怎麼補
       （這正是工單驗收 4：「故意漏一步 → 管線要擋下來並說出缺哪一站」）
  說明：比現行版本更舊的 `定版` 版本若缺 tag／release，只**黃字提醒**、不改離開碼——
       它們多半已被新版取代，硬擋等於「懲罰謹慎」（over-block 比 under-block 嚴重）。
       載入端落後（這個 session 跑的比 main 舊）也只提醒：那要 update＋重啟，
       是收貨端的動作，不是送貨端能修的，且 SessionStart 信標本來就一直在報。

離線測試：`scripts/test-release-check.sh`（走 `RELEASE_CHECK_FIXTURE`，不打 Gitea）。
"""
import json
import os
import re
import subprocess
import sys
import urllib.error
import urllib.request

HOST = os.environ.get("RELEASE_CHECK_HOST", "https://git.uncle6.me")
REPO = os.environ.get("RELEASE_CHECK_REPO", "inkstone/ISEP")
FIXTURE = os.environ.get("RELEASE_CHECK_FIXTURE", "")
HERE = os.path.dirname(os.path.abspath(__file__))


def die(msg, code=2):
    print(msg, file=sys.stderr)
    sys.exit(code)


def proj_root():
    """這支腳本操作的那個 ISEP repo 的根。"""
    v = os.environ.get("CLAUDE_PROJECT_DIR")
    if v and os.path.isfile(os.path.join(v, ".claude-plugin", "plugin.json")):
        return v
    return os.path.dirname(HERE)


def vtuple(v):
    nums = re.findall(r"\d+", v or "")
    return tuple(int(x) for x in nums[:3]) if nums else (0,)


# ══ 收貨端事實：全部從「別人」那裡拿，不聽送的人說 ═══════════════════════
def _fixture():
    try:
        with open(FIXTURE) as f:
            return json.load(f)
    except Exception:
        die("🔴 讀不到 RELEASE_CHECK_FIXTURE：%s" % FIXTURE)


def token():
    for env in ("GITEA_TOKEN_CLAUDE_CODE", "GITEA_TOKEN"):
        if os.environ.get(env):
            return os.environ[env]
    root = os.environ.get("CLAUDE_PROJECT_DIR") or proj_root()
    host = HOST.split("//")[-1].rstrip("/")
    try:
        out = subprocess.run(["git", "-C", root, "remote", "-v"],
                             capture_output=True, text=True, timeout=20).stdout
    except Exception:
        out = ""
    for line in out.splitlines():
        if host in line:
            m = re.search(r"//[^:/]+:([^@]+)@", line)
            if m:
                return m.group(1)
    return ""


def _api_names(path, key):
    """回一個 set：Gitea 上那個 collection 裡所有的名字（tag_name／name）。"""
    tk = token()
    if not tk:
        die("🔴 拿不到 %s 的 token（remote 沒帶憑證，也沒有 GITEA_TOKEN_CLAUDE_CODE）" % HOST)
    names, page = set(), 1
    while True:
        url = "%s/api/v1/repos/%s/%s?limit=50&page=%d" % (HOST, REPO, path, page)
        req = urllib.request.Request(url, headers={"Authorization": "token " + tk})
        try:
            rows = json.load(urllib.request.urlopen(req, timeout=25))
        except urllib.error.HTTPError as e:
            die("🔴 Gitea %s：%s" % (e.code, e.read().decode()[:200]))
        except Exception as e:
            die("🔴 連不上 %s：%s" % (HOST, e))
        if not rows:
            break
        for r in rows:
            n = r.get(key)
            if n:
                names.add(n)
        if len(rows) < 50:
            break
        page += 1
    return names


def gitea_tags():
    if FIXTURE:
        return set(_fixture().get("tags") or [])
    return _api_names("tags", "name")


def gitea_releases():
    if FIXTURE:
        return set(_fixture().get("releases") or [])
    return _api_names("releases", "tag_name")


# ══ 送貨端事實：main 上「定版」了哪些、現在宣稱哪一版、這個 session 載到哪一版 ══
def dingban_versions():
    """main 上有 `定版 vX.Y.Z` commit 的版本 —— 「應該被送到」的權威清單。"""
    if FIXTURE:
        return list(_fixture().get("dingban") or [])
    root = proj_root()
    try:
        out = subprocess.run(
            ["git", "-C", root, "log", "--format=%s", "--grep", "定版 v"],
            capture_output=True, text=True, timeout=30).stdout
    except Exception:
        out = ""
    seen, vers = set(), []
    for line in out.splitlines():
        m = re.search(r"定版\s+v?(\d+\.\d+\.\d+)", line)
        if m and m.group(1) not in seen:
            seen.add(m.group(1))
            vers.append(m.group(1))
    return vers


def current_version():
    """plugin.json 現在宣稱的那一版（＝ main 要送出去的那一版）。"""
    if FIXTURE:
        return _fixture().get("current") or ""
    try:
        with open(os.path.join(proj_root(), ".claude-plugin", "plugin.json")) as f:
            return json.load(f).get("version") or ""
    except Exception:
        return ""


def loaded_version():
    """這個 session 真的掛著的那一份 plugin 是哪一版（收貨端最後一段）。

    拿不到就回 None —— 不猜、不謊稱到了（house style：不知道就說不知道）。
    """
    if FIXTURE:
        return _fixture().get("loaded")
    root = os.environ.get("CLAUDE_PLUGIN_ROOT")
    if not root:
        return None
    try:
        with open(os.path.join(root, ".claude-plugin", "plugin.json")) as f:
            return json.load(f).get("version")
    except Exception:
        return None


# ══ 覆核：逐版問「到了嗎」 ════════════════════════════════════════════════
def audit():
    cur = current_version()
    if not cur:
        die("🔴 讀不到 .claude-plugin/plugin.json 的 version —— 這不是 ISEP repo，或檔案壞了。")
    dingban = dingban_versions()
    if cur not in dingban:
        # 現行版本連 `定版` commit 都沒有 ⇒ plugin.json 宣稱了一個沒被定版的號碼。
        # 這是 check-version-consistency 那條線的病，不是這支的守備範圍——但要講出來。
        dingban = [cur] + dingban
    tags = gitea_tags()
    rels = gitea_releases()

    def tagname(v):
        return "v" + v

    rows = []
    for v in sorted(set(dingban), key=vtuple, reverse=True):
        rows.append({
            "ver": v,
            "dingban": True,   # 這份清單本來就是從 定版 commit 來的
            "tag": tagname(v) in tags,
            "release": tagname(v) in rels,
            "current": v == cur,
        })
    return cur, rows


STATIONS = [
    ("dingban", "定版 commit", "在 main 上 commit 一顆 `定版 v<版本>`（＝ plugin.json 也改成那一版）"),
    ("tag", "tag（Gitea）", "打並推 tag：`git tag v<版本> && git push origin v<版本>`"),
    ("release", "release（Gitea）", "建 release：`scripts/release-ship v<版本>`（這一站就是最常斷的那站）"),
]


def report():
    cur, rows = audit()
    loaded = loaded_version()
    icon = {True: "✅", False: "❌"}
    lines = ["ISEP 運送鏈覆核（收貨端事實，不聽送的人說）  repo=%s" % REPO, ""]
    lines.append("  版本        定版  tag   release   （← ＝現行 plugin.json 宣稱的版本）")
    for r in rows:
        mark = " ←" if r["current"] else ""
        lines.append("  %-10s %s    %s     %s%s" % (
            "v" + r["ver"], icon[r["dingban"]], icon[r["tag"]], icon[r["release"]], mark))

    # ── 載入端（這個 session）──────────────────────────────────────────
    lines.append("")
    if loaded is None:
        lines.append("  載入端：這個 context 讀不到 CLAUDE_PLUGIN_ROOT ⇒ 說不出「這個 session 載到哪一版」"
                     "（不是 session 裡跑的，或沒裝 plugin）。")
    elif vtuple(loaded) < vtuple(cur):
        lines.append("  🟡 載入端落後：這個 session 掛的是 v%s，main 已到 v%s。" % (loaded, cur))
        lines.append("     修：本機 `claude plugin update isep@inkstone` **並重啟 session**"
                     "（升版與生效是兩件事）；雲端去動一下 Environment 的 setup script 逼它重拍快照。")
        lines.append("     這一段 SessionStart 信標本來就一直在報（beacon_report.py ①）。")
    else:
        lines.append("  ✅ 載入端：這個 session 掛的是 v%s，跟 main 宣稱的一致。" % loaded)

    # ── 判決 ────────────────────────────────────────────────────────────
    currow = next((r for r in rows if r["current"]), None)
    hard = []
    if currow:
        for key, label, fix in STATIONS:
            if not currow[key]:
                hard.append((label, fix))
    older_gaps = [r for r in rows if not r["current"] and (not r["tag"] or not r["release"])]

    lines.append("")
    if hard:
        lines.append("🔴 **現行版本 v%s 的送貨端斷了** —— 對 leo 等於這一版不存在：" % cur)
        for label, fix in hard:
            lines.append("     缺這一站：%s" % label)
            lines.append("        怎麼補：%s" % fix)
        if older_gaps:
            lines.append("   （另外還有更舊的版本也缺，見下方黃字，但那些不擋。）")
    else:
        lines.append("✅ 現行版本 v%s：定版、tag、release 都在 —— 送貨端完成，leo 那頁看得到。" % cur)

    if older_gaps:
        lines.append("")
        lines.append("🟡 更舊的定版版本也缺件（多半已被新版取代，只提醒、不擋）：")
        for r in older_gaps:
            miss = []
            if not r["tag"]:
                miss.append("tag")
            if not r["release"]:
                miss.append("release")
            lines.append("     v%s 缺：%s" % (r["ver"], "＋".join(miss)))
        lines.append("     要補歷史版本 ⇒ 逐版 `scripts/release-ship v<版本>`；"
                     "確定不補 ⇒ 這是 leo／總管的決定，記一句在 inkstone/ISEP#67。")

    out = "\n".join(lines)
    if hard:
        print(out, file=sys.stderr)
        return 2
    print(out)
    return 0


def main(argv):
    if argv and argv[0] in ("-h", "--help"):
        print(__doc__)
        return 0
    return report()


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
