#!/bin/bash
# Publish the CURRENT package.json version to the Gitea registry, from this machine.
#
# WHEN TO USE THIS
# Normal releases go through CI: `pnpm release` cuts a tag, the tag push triggers
# .gitea/workflows/publish.yml. Use this script only when that path is unavailable
# or inappropriate:
#   - the one-off INFRA-F73 migration: getting the already-released 1.1.0 into the
#     new registry under the new name, WITHOUT inventing a 1.2.0 nobody asked for
#     (the v1.1.0 tag points at pre-rename commits, so re-tagging it would rewrite
#     what that tag means — publishing locally leaves tag history alone);
#   - the act_runner or the Gitea host being down at release time (this is the
#     "publish locally" fallback that backlog INFRA-F71 refers to).
#
# It runs the same gates CI would (`prepublishOnly`, via pnpm's own hook) so this
# is not a way to sneak past them.
#
# CREDENTIAL — a *Gitea* token with read:package + write:package. NOT a GitHub PAT
# (Gitea rejects `ghp_*` outright at authGroup.Verify — measured, INFRA-F73).
# Supply it either way:
#     export GITEA_PACKAGES_TOKEN='...'   ./scripts/publish-to-gitea.sh
#     # or store it once, readable only by you:
#     mkdir -p ~/.config/tvu && pbpaste > ~/.config/tvu/gitea-token && chmod 600 ~/.config/tvu/gitea-token
#
# The token is written to a temporary npmrc that is deleted on exit — never to
# ~/.npmrc, so it does not linger on disk or leak into unrelated installs.

set -uo pipefail

# INFRA-F74 (2026-07-30): registry 走 https 标准 443 + /gitea 子路径。
# REG_ORIGIN 不含 scheme —— npmrc 的 auth key 用它；REG_PREFIX 是 Gitea 在
# 主机 :443 反代下的挂载点。老入口 http://…:3001 仍在（回退路径），刻意不用。
REG_ORIGIN="product-demo.tvustream.com"
REG_PREFIX="/gitea"
REG_HOST="$REG_ORIGIN$REG_PREFIX"
REG="https://$REG_HOST/api/packages/ux-team/npm/"
TOKEN_FILE="$HOME/.config/tvu/gitea-token"

cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1

die() { echo "❌ $*" >&2; exit 1; }

# --- credential ---------------------------------------------------------------
TOKEN="${GITEA_PACKAGES_TOKEN:-}"
if [ -z "$TOKEN" ] && [ -r "$TOKEN_FILE" ]; then
  TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
fi
[ -n "$TOKEN" ] || die "no token. Set GITEA_PACKAGES_TOKEN, or put one in $TOKEN_FILE (chmod 600).
   Create it in Gitea: avatar -> Settings -> Applications -> Generate New Token,
   ticking read:package AND write:package."

case "$TOKEN" in
  ghp_*|github_pat_*|gho_*)
    die "that is a GitHub token, not a Gitea one. Gitea will refuse it (authGroup.Verify).
   Create a Gitea token instead: avatar -> Settings -> Applications." ;;
esac

NAME="$(node -p "require('./package.json').name")"
VERSION="$(node -p "require('./package.json').version")"
CONFIGURED_REG="$(node -p "require('./package.json').publishConfig.registry")"

echo "package : $NAME@$VERSION"
echo "registry: $CONFIGURED_REG"
[ "$CONFIGURED_REG" = "$REG" ] || echo "⚠️  package.json publishConfig.registry differs from this script's default — using package.json's."

# --- pre-flight: can the credential actually WRITE? ---------------------------
# Fixed 2026-07-30. This used to read an absent package and accept HTTP 404 as
# "credential OK" — but reading an absent package only needs `read:package`, so a
# read-only PAT passed here and then died at `pnpm publish` with a bare 401, which
# is the very thing a pre-flight exists to prevent. Same fake-gate class as the two
# this repo has already burned on (render-gate, first cut of audit:on-fill-content-color).
# A credential gate must exercise the verb it gates, so: read, then really write,
# then clean up and verify the cleanup by re-reading the list.
#
# Also corrected here: `reqPackageAccess` does NOT pin down which package scope is
# missing — measured 2026-07-30, a PAT lacking `read:package` reports it too. Step A
# is what disambiguates.
body="$(mktemp)"; probe_payload="$(mktemp)"
trap 'rm -f "$body" "$probe_payload" "${NPMRC:-}"' EXIT
API_V1="https://$REG_HOST/api/v1/packages/ux-team"
PROBE_BASE="https://$REG_HOST/api/packages/ux-team/generic/publish-preflight-probe/0.0.0-preflight"

echo -n "pre-flight A (read:package): "
code="$(curl -s -o "$body" -w '%{http_code}' --max-time 20 \
  -H "Authorization: token $TOKEN" "$API_V1")"
case "$code" in
  200) echo "OK (HTTP 200)" ;;
  401|403)
    echo "REJECTED (HTTP $code)"
    echo "   Gitea said: $(head -c 200 "$body")" >&2
    die "token rejected reading packages. 'authGroup.Verify' = wrong system's token (a GitHub
   ghp_* PAT does this); 'required scope(s): [read:package]' = Gitea PAT without read:package.
   Token scopes are immutable after creation — delete and regenerate, you cannot edit them." ;;
  000) die "cannot reach $REG_HOST — is the host up? (the monitor in ~/Documents/AICoding/VS_Code/tvu-host-monitor tracks this)" ;;
  *) die "unexpected HTTP $code from $API_V1: $(head -c 200 "$body")" ;;
esac

echo -n "pre-flight B (write:package, real write): "
printf 'publish pre-flight probe — safe to delete\n' > "$probe_payload"
code="$(curl -s -o "$body" -w '%{http_code}' --max-time 30 \
  -H "Authorization: token $TOKEN" --upload-file "$probe_payload" "$PROBE_BASE/probe.txt")"
case "$code" in
  201|409) echo "OK (HTTP $code)" ;;
  401|403)
    echo "REJECTED (HTTP $code)"
    echo "   Gitea said: $(head -c 200 "$body")" >&2
    die "the token can read packages but cannot write them. 'reqPackageAccess' = valid Gitea
   credential whose package scope is insufficient (it does not say which one — step A above
   already proved read:package works, so what is missing is write:package)." ;;
  404|405) die "the generic-registry upload route does not match on this Gitea ($PROBE_BASE).
   STOP — do not paper over this by editing the URL until something returns 200; a 200 obtained
   that way is not evidence of write:package. Check the instance's API version first." ;;
  *) die "unexpected HTTP $code writing the probe: $(head -c 200 "$body")" ;;
esac

# Cleanup is loud but non-fatal — the credential is proven either way, and refusing
# to publish over a stray probe package would be the wrong trade.
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 -X DELETE \
  -H "Authorization: token $TOKEN" "$API_V1/generic/publish-preflight-probe/0.0.0-preflight")"
if [ "$code" = "204" ] && ! curl -s --max-time 20 -H "Authorization: token $TOKEN" "$API_V1" \
     | grep -q 'publish-preflight-probe'; then
  echo "pre-flight C (cleanup): probe removed, verified by re-reading the list (not by trusting 204)"
else
  echo "⚠️  pre-flight probe cleanup returned HTTP $code and/or the package is still listed —" >&2
  echo "   delete 'publish-preflight-probe' by hand at https://$REG_HOST/ux-team/-/packages" >&2
fi

# --- already published? -------------------------------------------------------
enc_name="$(printf '%s' "$NAME" | sed 's#/#%2f#')"
existing="$(curl -s --max-time 20 -H "Authorization: token $TOKEN" "${REG%/}/$enc_name" \
  | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const j=JSON.parse(d);console.log(Object.keys(j.versions||{}).join(' '))}catch(e){console.log('')}})")"
if [ -n "$existing" ]; then
  echo "already on the registry: $existing"
  case " $existing " in
    *" $VERSION "*) die "$NAME@$VERSION is already published. Gitea refuses to overwrite an existing name@version — bump the version if you meant to republish." ;;
  esac
fi

# --- confirm ------------------------------------------------------------------
if [ "${YES:-}" != "1" ]; then
  printf 'Publish %s@%s to %s ? [y/N] ' "$NAME" "$VERSION" "$REG_HOST"
  read -r ans
  case "$ans" in y|Y) ;; *) echo "aborted."; exit 0 ;; esac
fi

# --- publish ------------------------------------------------------------------
# Temp npmrc, never ~/.npmrc. pnpm v10 treats ${VAR} in npmrc as a literal, so the
# token is written expanded — which is precisely why this file is temporary.
NPMRC="$(mktemp)"
{
  echo "@ux-team:registry=$CONFIGURED_REG"
  echo "//$REG_HOST/api/packages/ux-team/npm/:_authToken=$TOKEN"
} > "$NPMRC"
chmod 600 "$NPMRC"

# Rebuild first. `dist/` is gitignored and only regenerated by `prepare` (which
# pnpm runs on install), so a working tree that changed source without reinstalling
# carries a STALE dist — and `prepublishOnly`'s audit:composition-exports catches
# exactly that, aborting the publish. Hit while migrating: dist still held the old
# package name. `prepare` rather than `build` on purpose: `build` also runs
# build:playground, which rewrites the committed playground-dist/ — that belongs to
# RELEASING Step 4 as a reviewed visual commit, not to a publish script.
echo "rebuilding dist (prepare: typecheck + build + token/composition/icon exports + build:wc)…"
if ! pnpm run prepare; then
  die "build failed — nothing was published. Fix the build before retrying."
fi

echo "publishing (prepublishOnly gates run again inside pnpm publish)…"
npm_config_userconfig="$NPMRC" pnpm publish --no-git-checks
rc=$?
[ $rc -eq 0 ] || die "pnpm publish failed (exit $rc). Nothing was published."

# --- verify -------------------------------------------------------------------
# Publishing "succeeding" is not proof: v0.1.0/v0.1.1 once silently failed for
# weeks. Read the registry back.
echo ""
echo "verifying against the registry…"
after="$(curl -s --max-time 20 -H "Authorization: token $TOKEN" "${REG%/}/$enc_name" \
  | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const j=JSON.parse(d);console.log(Object.keys(j.versions||{}).join(' '))}catch(e){console.log('<unparseable>')}})")"
echo "versions now present: $after"
case " $after " in
  *" $VERSION "*)
    echo "✅ $NAME@$VERSION is live on Gitea."
    echo "   Now LOOK AT THE PACKAGES PAGE with your own eyes — hard rule, the API being"
    echo "   green has never been accepted as sufficient here:"
    echo "   https://$REG_HOST/ux-team/-/packages" ;;
  *) die "publish reported success but $VERSION is NOT in the registry listing. Investigate before assuming it worked." ;;
esac
