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

工單抓到的斷點，一句話：**沒有任何東西會建 release。**
    ISEP repo 全樹 grep「建 release」的 API 呼叫 → 0 命中。
    後果：v0.22.0 的 tag 早就推了，Gitea Releases 頁卻停在 v0.18.0
    ⇒ 對 leo 等於這一版不存在（「ISEP 現在是哪一版」只有 Releases 答得出來）。

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

🔴 送之前，三件事「必須在場」才放行（require-something-present，不是關鍵字黑名單）：
   ① 版本一致：`plugin.json` 的 version ＝ 這個 tag 的號碼
      —— 直接跑既有的 `scripts/check-version-consistency.sh`，**不另寫一套判準**。
   ② tag 真的在 Gitea 上：去問收貨端，不在就不給建 release（先去推 tag）。
   ③ release note 有內容：沒給 `--notes-file` 就從 `git log <上一個release>..<tag>`
      自動生一份**事實**清單（哪幾顆 commit）——leo 可以再改，但不留白。

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

冪等：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

HOST = os.environ.get("RELEASE_SHIP_HOST", "https://git.uncle6.me")
REPO = os.environ.get("RELEASE_SHIP_REPO", "inkstone/ISEP")
FIXTURE = os.environ.get("RELEASE_SHIP_FIXTURE", "")
HERE = os.path.dirname(os.path.abspath(__file__))
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 _fixture():
    try:
        with open(FIXTURE) as f:
            return json.load(f)
    except Exception:
        die("🔴 讀不到 RELEASE_SHIP_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 check_consistency():
    if FIXTURE:
        fx = _fixture()
        return bool(fx.get("consistency_ok", True)), fx.get("consistency_msg", "")
    script = os.path.join(proj_root(), "scripts", "check-version-consistency.sh")
    if not os.path.isfile(script):
        return False, "找不到 scripts/check-version-consistency.sh —— 這不是 ISEP repo？"
    p = subprocess.run(["sh", script], capture_output=True, text=True, timeout=30)
    return p.returncode == 0, (p.stdout + p.stderr).strip()


# ══ 前置條件 ② tag 在不在 Gitea（問收貨端）═════════════════════════════════
def gitea_has_tag(tagname):
    if FIXTURE:
        return tagname in set(_fixture().get("tags") or [])
    tk = token()
    if not tk:
        die("🔴 拿不到 %s 的 token（remote 沒帶憑證，也沒有 GITEA_TOKEN_CLAUDE_CODE）" % HOST)
    url = "%s/api/v1/repos/%s/tags/%s" % (HOST, REPO, tagname)
    req = urllib.request.Request(url, headers={"Authorization": "token " + tk})
    try:
        urllib.request.urlopen(req, timeout=25)
        return True
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return False
        die("🔴 Gitea %s：%s" % (e.code, e.read().decode()[:200]))
    except Exception as e:
        die("🔴 連不上 %s：%s" % (HOST, e))


def gitea_release(tagname):
    """回這個 tag 的 release dict，沒有回 None。"""
    if FIXTURE:
        rels = _fixture().get("releases") or []
        if tagname in rels:
            return {"tag_name": tagname, "html_url": "(fixture)"}
        return None
    tk = token()
    url = "%s/api/v1/repos/%s/releases/tags/%s" % (HOST, REPO, tagname)
    req = urllib.request.Request(url, headers={"Authorization": "token " + tk})
    try:
        return json.load(urllib.request.urlopen(req, timeout=25))
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return None
        die("🔴 Gitea %s：%s" % (e.code, e.read().decode()[:200]))
    except Exception as e:
        die("🔴 連不上 %s：%s" % (HOST, e))


# ══ 前置條件 ③ release note ════════════════════════════════════════════════
def latest_release_before(ver):
    """比 ver 舊、且在 Gitea 上有 release 的最新那一版 tag（拿來當 git log 起點）。"""
    if FIXTURE:
        rels = _fixture().get("releases") or []
    else:
        tk = token()
        url = "%s/api/v1/repos/%s/releases?limit=50" % (HOST, REPO)
        req = urllib.request.Request(url, headers={"Authorization": "token " + tk})
        try:
            rels = [r.get("tag_name") for r in
                    json.load(urllib.request.urlopen(req, timeout=25))]
        except Exception:
            rels = []

    def vt(v):
        n = re.findall(r"\d+", v or "")
        return tuple(int(x) for x in n[:3]) if n else (0,)
    older = [t for t in rels if t and vt(t) < vt(ver)]
    return max(older, key=vt) if older else None


def dingban_title(ver):
    """這個版本的 `定版 vX.Y.Z（…）` commit 裡括號那句話 —— 拿來當 release 名，
    對齊 leo 既有 release 的命名慣例（`vX.Y.Z — 一句人話`）。找不到就回純 tag 名。"""
    if FIXTURE:
        return None
    root = proj_root()
    try:
        out = subprocess.run(
            ["git", "-C", root, "log", "--format=%s", "--grep", "定版 v%s" % ver],
            capture_output=True, text=True, timeout=30).stdout
    except Exception:
        out = ""
    for line in out.splitlines():
        m = re.search(r"定版\s+v%s[（(]([^）)]+)[）)]" % re.escape(ver), line)
        if m:
            return m.group(1).strip()
    return None


def auto_notes(tagname):
    """從 git log 生一份事實 note：上一個 release 之後到這個 tag 之間的 commit。"""
    root = proj_root()
    since = latest_release_before(tagname)
    rng = ("%s..%s" % (since, tagname)) if since else tagname
    try:
        out = subprocess.run(
            ["git", "-C", root, "log", "--no-merges", "--format=- %s", rng],
            capture_output=True, text=True, timeout=30)
        body = out.stdout.strip()
    except Exception:
        body = ""
    head = ("這一版收了以下改動（%s 之後）：" % since) if since \
        else "這一版收了以下改動："
    if not body:
        body = "-（git log 取不到 commit 清單，請手動補；release 已建起來，leo 可直接編輯。）"
    return ("%s\n\n%s\n\n---\n（本 note 由 scripts/release-ship 從 git log 自動生成，"
            "leo／總管可直接在 Gitea 上編輯。inkstone/ISEP#67）" % (head, body))


# ══ 真的建 release（POST 收貨端）═══════════════════════════════════════════
def create_release(tagname, notes, dry, name=None):
    name = name or tagname
    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 FIXTURE:
        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 = token()
    if not tk:
        die("🔴 拿不到 %s 的 token —— 建 release 是寫入，一定要憑證。" % 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():
    """跑 release-check，拿收貨端事實覆核。回 (ok, 輸出)。"""
    check = os.path.join(HERE, "release-check")
    env = dict(os.environ)
    if FIXTURE:
        # fixture 模式下，把剛「建」進 sink 的 release 併進 release-check 的 fixture 視圖
        fx = _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 = (os.environ.get("RELEASE_SHIP_SINK") or "/tmp/rs") + ".checkfx.json"
        json.dump(merged, open(cf, "w"))
        env["RELEASE_CHECK_FIXTURE"] = cf
    try:
        p = subprocess.run(["python3", check], 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, ""]

    # 前置 ①
    ok, msg = check_consistency()
    if not ok:
        die("🚫 前置①版本一致性沒過 —— 不給建 release。\n%s\n"
            "   （plugin.json 的 version 要等於 %s。這一步重用 "
            "scripts/check-version-consistency.sh 的判準，不另立一套。）" % (msg, tagname))
    out.append("✅ 前置① 版本一致（plugin.json ＝ %s）" % tagname)

    # 前置 ②
    if not gitea_has_tag(tagname):
        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))
    out.append("✅ 前置② Gitea 上有 tag %s（問過收貨端）" % tagname)

    # 冪等
    existing = gitea_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()
            out.append("")
            out.append("── 覆核（release-check，問收貨端）──")
            out.append(vout)
            code = 0 if vok else 2
        return code, "\n".join(out)

    # 前置 ③
    if notes_file:
        with open(notes_file) as f:
            notes = f.read()
        out.append("✅ 前置③ release note 來自 %s" % notes_file)
    else:
        notes = auto_notes(tagname)
        out.append("✅ 前置③ release note 由 git log 自動生成（leo 可再改）")

    # 建（release 名對齊 leo 慣例：vX.Y.Z — 定版 commit 那句人話）
    title = dingban_title(ver)
    relname = ("%s — %s" % (tagname, title)) if title else tagname
    rel, how = create_release(tagname, notes, dry, name=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()
        out.append("")
        out.append("── 覆核（release-check，問收貨端，不是問自己）──")
        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:]))
