#!/usr/bin/env python3
"""release-ship — 把「已經推上去的 tag」變成「leo 那頁看得到的 release」
                 （inkstone/ISEP#67，對應 arcrun-rag 的 release-record 站）

工單抓到的斷點，一句話：**沒有任何東西會建 release。**
    ISEP repo 全樹 grep「建 release」的 API 呼叫 → 0 命中。
    後果：tag 早就推了，Gitea Releases 頁卻停在舊版
    ⇒ 對 leo 等於這一版不存在（「ISEP 現在是哪一版」只有 Releases 答得出來）。
    08-27 v0.4.0／v0.5.0、09-02 v0.22.0、09-07 v0.23.0／v0.24.0——同一站斷了三輪。

這支就是補上那一站。它**只做 tag → release** 這一段，不打 tag、不改 plugin.json、
不碰 main —— 打 tag 是 `release-tag-guard.sh` 在管的、總管驗過整個 milestone 之後的動作。

🔴 送之前，三件事「必須在場」才放行（require-something-present，不是關鍵字黑名單）：
   ① **那個 tag 的樹**裡 `plugin.json` 的 version ＝ 這個 tag 的號碼。
      比對的對象是 tag 自己的樹（問 Gitea `contents/…?ref=<tag>`），**不是工作樹**。
      🔴 09-05 那版拿 `check-version-consistency.sh`（工作樹 plugin.json vs 本機最新 tag）
      當前置①，結果對「不是最新的那個 tag」永遠建不了：總管在 v0.23.0 的 checkout
      跑 `release-ship v0.23.0` 被擋，訊息說最新 tag 是 v0.24.0——而 v0.23.0 的樹
      明明就一致。**要送的東西是 tag，判它的就該看它的樹**；工作樹可能是任何分支
      （這支本身就是在一條 feature 分支上寫的），跟那個 tag 沒有關係。
      `check-version-consistency.sh` 守的是「main 上 plugin.json 跟最新 tag 一致」，
      那是打 tag 那一刻的事（release-tag-guard.sh），不是建 release 這一刻的事。
   ② tag 真的在 Gitea 上：去問收貨端，不在就不給建 release（先去推 tag）。
   ③ release note 有內容：沒給 `--notes-file` 就從 Gitea「上一個 release ⋯ 這個 tag」
      的 compare 自動生一份**事實**清單（哪幾顆 commit，不含 merge）——leo 可以再改，但不留白。
      問 Gitea 而不是本機 git log：這支從 plugin 快取目錄跑時**旁邊沒有 git repo**。

🔴 送完**自己不算數**：最後回頭跑一次 `release-check <這個 tag>`，拿收貨端（Gitea releases 頁）
   的事實覆核「真的建起來了」。送的人說成功不算，問收貨的那一端才算
   （工單：「問的人不能是送的人」）。

讀全部匿名（D20），只有 POST 建 release 才要 token
（`GITEA_TOKEN_CLAUDE_CODE`／`GITEA_TOKEN`，或這個 repo remote URL 裡帶的那把）。

冪等：release 已經在了 ⇒ 報「本來就在」、離開碼 0，不重複建、不報錯、**不動它的內容**。

離開碼：0 ＝ release 在 Gitea 上了（新建的或本來就在）
        2 ＝ 某個前置條件沒滿足（stderr 講出是哪一個＋怎麼補）

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

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_SHIP_HOST", rc.DEFAULT_HOST)
REPO = os.environ.get("RELEASE_SHIP_REPO", rc.DEFAULT_REPO)
FIXTURE = os.environ.get("RELEASE_SHIP_FIXTURE", "")
VER_RE = re.compile(r"^v?(\d+\.\d+\.\d+)$")


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


def proj_root():
    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_SHIP_FIXTURE：%s" % FIXTURE)


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


def ask(fn, *a):
    """問收貨端；問不到就死（不猜）。"""
    try:
        return fn(*a)
    except rc.ChainError as e:
        die("🔴 問不到收貨端 %s：%s" % (HOST, e))


# ══ 前置條件 ③ release note ════════════════════════════════════════════
def auto_notes(tagname, releases):
    """從 Gitea compare 生一份事實 note：上一個 release 之後到這個 tag 之間的 commit。"""
    since = CHAIN.latest_release_before(tagname, releases)
    subjects = ask(CHAIN.commits_between, since, tagname)
    head = ("這一版收了以下改動（%s 之後）：" % since) if since else "這一版收了以下改動："
    body = "\n".join("- %s" % s for s in subjects)
    if not body:
        body = "-（Gitea 取不到 commit 清單，請手動補；release 已建起來，leo 可直接編輯。）"
    return ("%s\n\n%s\n\n---\n（本 note 由 scripts/release-ship 從 git log 自動生成，"
            "leo／總管可直接在 Gitea 上編輯。inkstone/ISEP#67）" % (head, body))


def release_name(tagname):
    """`vX.Y.Z — 定版 commit 括號那句人話`，對齊 leo 既有 release 的命名慣例；找不到就純 tag 名。"""
    ver = tagname.lstrip("v")
    msg = ask(CHAIN.tag_commit_message, tagname)
    title = rc.dingban_title(msg, ver)
    if not title:
        for s in ask(CHAIN.commits_between, None, tagname)[-20:]:
            title = rc.dingban_title(s, ver)
            if title:
                break
    return ("%s — %s" % (tagname, title)) if title else tagname


# ══ 真的建 release（POST 收貨端）═══════════════════════════════════════════
def create_release(tagname, notes, dry, name):
    payload = {"tag_name": tagname, "name": name, "body": notes,
               "draft": False, "prerelease": False}
    if dry:
        return None, "🧪 --dry-run：不 POST。要建的 release payload：\n%s" % \
            json.dumps({"tag_name": tagname, "name": name,
                        "body_lines": notes.count("\n") + 1}, ensure_ascii=False)
    if FX is not None:
        sink = os.environ.get("RELEASE_SHIP_SINK")
        if sink:
            try:
                cur = json.load(open(sink)) if os.path.exists(sink) else []
            except Exception:
                cur = []
            cur.append(tagname)
            json.dump(cur, open(sink, "w"))
        return {"tag_name": tagname, "html_url": "(fixture-created)"}, "（fixture 模式，未打網路）"
    tk = rc.token_from_env_or_remote(HOST, proj_root())
    if not tk:
        die("🔴 拿不到 %s 的 token —— 建 release 是寫入，一定要憑證"
            "（GITEA_TOKEN_CLAUDE_CODE，或這個 repo 的 remote URL 帶憑證）。" % HOST)
    url = "%s/api/v1/repos/%s/releases" % (HOST, REPO)
    req = urllib.request.Request(
        url, data=json.dumps(payload).encode(), method="POST",
        headers={"Authorization": "token " + tk, "Content-Type": "application/json"})
    try:
        r = json.load(urllib.request.urlopen(req, timeout=40))
        return r, "已建立"
    except urllib.error.HTTPError as e:
        die("🔴 Gitea 建 release 失敗 %s：%s" % (e.code, e.read().decode()[:300]))
    except Exception as e:
        die("🔴 連不上 %s：%s" % (HOST, e))


# ══ 送完覆核：回頭問收貨端（不是問自己）══════════════════════════════════
def verify_via_check(tagname):
    """跑 `release-check <tag>`，拿收貨端事實覆核**這一版**。回 (ok, 輸出)。"""
    check = os.path.join(HERE, "release-check")
    env = dict(os.environ)
    if FX is not None:
        # fixture 模式下，把剛「建」進 sink 的 release 併進 release-check 的 fixture 視圖
        sink = os.environ.get("RELEASE_SHIP_SINK")
        extra = []
        if sink and os.path.exists(sink):
            try:
                extra = json.load(open(sink))
            except Exception:
                extra = []
        merged = dict(FX)
        merged["releases"] = sorted(set((FX.get("releases") or []) + extra))
        cf = (sink or "/tmp/rs") + ".checkfx.json"
        json.dump(merged, open(cf, "w"))
        env["RELEASE_CHECK_FIXTURE"] = cf
    env.setdefault("RELEASE_CHECK_HOST", HOST)
    env.setdefault("RELEASE_CHECK_REPO", REPO)
    try:
        p = subprocess.run(["python3", check, tagname], capture_output=True, text=True,
                           timeout=60, env=env)
        return p.returncode == 0, (p.stdout + p.stderr).strip()
    except Exception as e:
        return False, "跑不動 release-check：%s" % e


# ══ 主流程 ════════════════════════════════════════════════════════════════
def ship(arg, notes_file=None, dry=False, do_verify=True):
    m = VER_RE.match(arg.strip())
    if not m:
        die("🔴 版本寫法是 vX.Y.Z 或 X.Y.Z，你給的是：%s" % arg)
    ver = m.group(1)
    tagname = "v" + ver
    out = ["📦 release-ship %s（把 tag 變成 leo 看得到的 release）" % tagname, ""]

    tags = ask(CHAIN.tags)
    # 前置 ②（先問 tag 在不在——①要讀它的樹，tag 不在就沒有樹可讀）
    if tagname not in tags:
        die("🚫 前置② Gitea 上沒有 tag %s —— 不給建 release。\n"
            "   release 是「把已存在的 tag 公開成一頁」，tag 都還沒推，先推：\n"
            "        git tag %s   &&   git push origin %s\n"
            "   （推 tag 那一刻 release-tag-guard.sh 會擋下版本對不上的 tag。）"
            % (tagname, tagname, tagname))
    # 前置 ①（看 tag 自己的樹，不看工作樹）
    at_tag = ask(CHAIN.manifest_version_at, tagname)
    if at_tag != ver:
        die("🚫 前置① tag %s 的樹裡 plugin.json 寫的是「%s」—— 不給建 release。\n"
            "   建 release 是把這個 tag 公開給 leo 裝；tag 的樹自己就講錯版本，裝了會各說各話。\n"
            "   這種 tag 不該存在（release-tag-guard.sh 在推 tag 那一刻就會擋），\n"
            "   修法：刪掉這個 tag、在 main 上把 plugin.json 改成 %s 重新定版、再打一次 tag。"
            % (tagname, at_tag or "（讀不到）", ver))
    out.append("✅ 前置② Gitea 上有 tag %s（問過收貨端）" % tagname)
    out.append("✅ 前置① tag %s 的樹裡 plugin.json ＝ %s（看的是 tag 的樹，不是工作樹）" % (tagname, ver))

    # 冪等
    existing = ask(CHAIN.release, tagname)
    if existing:
        out.append("ℹ️ release %s 本來就在了（%s）—— 不重複建、不動它。"
                   % (tagname, existing.get("html_url") or ""))
        code = 0
        if do_verify:
            vok, vout = verify_via_check(tagname)
            out.append("")
            out.append("── 覆核（release-check %s，問收貨端）──" % tagname)
            out.append(vout)
            code = 0 if vok else 2
        return code, "\n".join(out)

    # 前置 ③
    releases = ask(CHAIN.releases)
    if notes_file:
        with open(notes_file) as f:
            notes = f.read()
        out.append("✅ 前置③ release note 來自 %s" % notes_file)
    else:
        notes = auto_notes(tagname, releases)
        out.append("✅ 前置③ release note 由 Gitea 的 commit 清單自動生成（leo 可再改）")

    # 建（release 名對齊 leo 慣例：vX.Y.Z — 定版 commit 那句人話）
    relname = release_name(tagname)
    rel, how = create_release(tagname, notes, dry, relname)
    if dry:
        out.append("   release 名：%s" % relname)
        out.append("")
        out.append(how)
        return 0, "\n".join(out)
    out.append("✅ 已建立 release「%s」（%s）" % (relname, (rel or {}).get("html_url") or how))

    # 覆核（送完問收貨端）
    code = 0
    if do_verify:
        vok, vout = verify_via_check(tagname)
        out.append("")
        out.append("── 覆核（release-check %s，問收貨端，不是問自己）──" % tagname)
        out.append(vout)
        code = 0 if vok else 2
    return code, "\n".join(out)


def main(argv):
    arg, notes_file, dry, do_verify = None, None, False, True
    i = 0
    while i < len(argv):
        a = argv[i]
        if a in ("-h", "--help"):
            print(__doc__)
            return 0
        elif a == "--notes-file":
            i += 1
            notes_file = argv[i]
        elif a == "--dry-run":
            dry = True
        elif a == "--no-verify":
            do_verify = False
        elif a.startswith("-"):
            die("不認得的參數：%s（--help 看用法）" % a)
        else:
            arg = a
        i += 1
    if not arg:
        die("用法：release-ship <版本 vX.Y.Z> [--notes-file <檔>] [--dry-run] [--no-verify]")
    code, report = ship(arg, notes_file, dry, do_verify)
    print(report, file=(sys.stdout if code == 0 else sys.stderr))
    return code


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