#!/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 也還顯示舊版。
2026-09-07 又一次：v0.23.0／v0.24.0 打了 tag，Releases 頁停在 v0.22.0——
    交棒給 leo 的「測試位置 releases/tag/v0.24.0」打開是一頁空 tag。

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

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

🔴 這支的全部價值在一句話：**它不聽上一站說「我送了」，它自己去問收貨的那一端。**
   tag 在不在 → 問 Gitea 的 tags；release 在不在 → 問 Gitea 的 releases（leo 會看的那頁）；
   載入到了沒 → 看這個 session 真的掛著的那一份 plugin.json。
   問的方式全部匿名（D20：讀不帶憑證），判準與 release-ship／SessionStart 信標同一份
   （hooks/lib/release_chain.py）。

用法：
    release-check              現行版本 ＝ 這棵樹 plugin.json 宣稱的那一版
    release-check v0.23.0      現行版本 ＝ 你指定的那一版（release-ship 送完拿來覆核**那一版**，
                               不管工作樹現在 checkout 在哪）

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

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

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

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "hooks", "lib"))
try:
    import release_chain as rc
except Exception as e:                       # pragma: no cover
    print("🔴 讀不到 hooks/lib/release_chain.py：%s" % e, file=sys.stderr)
    sys.exit(2)

HOST = os.environ.get("RELEASE_CHECK_HOST", rc.DEFAULT_HOST)
REPO = os.environ.get("RELEASE_CHECK_REPO", rc.DEFAULT_REPO)
FIXTURE = os.environ.get("RELEASE_CHECK_FIXTURE", "")


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 load_fixture():
    if not FIXTURE:
        return None
    try:
        with open(FIXTURE) as f:
            return json.load(f)
    except Exception:
        die("🔴 讀不到 RELEASE_CHECK_FIXTURE：%s" % FIXTURE)


FX = load_fixture()
CHAIN = rc.Chain(HOST, REPO, fixture=FX)

# 🔴 裁定不補的歷史版本（唯一識別碼往放行方向比對，不是關鍵字）：
#   總管 2026-09-07（inkstone/ISEP#67 → comment 6574）：「v0.19／0.20／0.21 不補——
#   沒 tag，v0.22.0 的 note 已列到那三個定版。」這三個定版 commit 永遠在 main 上，
#   不列在這裡它們就會每次都黃字——天天念的東西會被學會忽略，等於不存在。
WAIVED = {"0.19.0": "inkstone/ISEP#67 → 6574", "0.20.0": "inkstone/ISEP#67 → 6574",
          "0.21.0": "inkstone/ISEP#67 → 6574"}


# ══ 送貨端事實：main 上「定版」了哪些、現在宣稱哪一版、這個 session 載到哪一版 ══
def dingban_versions():
    """main 上有 `定版 vX.Y.Z` commit 的版本 —— 「應該被送到」的權威清單。"""
    if FX is not None:
        return list(FX.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 FX is not None:
        return FX.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 FX is not None:
        return FX.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(focus=None):
    cur = focus or 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
    try:
        tags = CHAIN.tags()
        rels = CHAIN.releases()
    except rc.ChainError as e:
        die("🔴 問不到收貨端，這一支不猜：%s" % e)

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


def ship_cmd():
    """補 release 的那一行要能直接貼著跑：印正本自己的絕對路徑（mistakes.md 2026-08-31）。"""
    return "python3 %s" % os.path.join(HERE, "release-ship")


def report(focus=None):
    cur, rows = audit(focus)
    loaded = loaded_version()
    icon = {True: "✅", False: "❌"}
    lines = ["ISEP 運送鏈覆核（收貨端事實，不聽送的人說）  repo=%s" % REPO, ""]
    lines.append("  版本        定版  tag   release   （← ＝%s）"
                 % ("你指定的版本" if focus else "現行 plugin.json 宣稱的版本"))
    for r in rows:
        mark = " ←" if r["current"] else ""
        if r["waived"] and r["gaps"]:
            mark = "  ⏭ 裁定不補（%s）" % r["waived"]
        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 rc.vtuple(loaded) < rc.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 = currow["gaps"] if currow else []
    older_gaps = [r for r in rows if not r["current"] and r["gaps"] and not r["waived"]]

    lines.append("")
    if hard:
        lines.append("🔴 **現行版本 v%s 的送貨端斷了** —— 對 leo 等於這一版不存在：" % cur)
        for _key, label, fix in hard:
            lines.append("     缺這一站：%s" % label)
            lines.append("        怎麼補：%s" % fix.format(v=cur, ship=ship_cmd()))
        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:
            lines.append("     v%s 缺：%s" % (r["ver"], "＋".join(label for _k, label, _f in r["gaps"])))
        lines.append("     要補歷史版本 ⇒ 逐版 `%s v<版本>`；"
                     "確定不補 ⇒ 這是 leo／總管的決定，記一句在 inkstone/ISEP#67"
                     "（v0.19／0.20／0.21 已於 2026-09-07 裁定不補）。" % ship_cmd())

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


def main(argv):
    focus = None
    for a in argv:
        if a in ("-h", "--help"):
            print(__doc__)
            return 0
        m = re.match(r"^v?(\d+\.\d+\.\d+)$", a.strip())
        if not m:
            die("用法：release-check [vX.Y.Z]（--help 看說明）；不認得：%s" % a)
        focus = m.group(1)
    return report(focus)


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