#!/usr/bin/env bash
# Shared release pipeline for the ee-extensions plugin family.
#
# SOURCE OF TRUTH: this file lives in the Manifest Updater repo under .ci/ and is
# mirrored to foh-updates/.ci/ by MU's release workflow (the "seed" job). Each
# plugin's thin release.yml fetches it from the public channel and runs it.
#
# Run from a plugin checkout (CWD = the plugin repo at the tagged commit).
# Expects env: GITHUB_REPOSITORY, GITHUB_REF_NAME, GITHUB_SERVER_URL, GITHUB_TOKEN.
# Optional env: FOH_UPDATES_DEPLOY_KEY (+ FOH_UPDATES_SSH_PORT) to publish to the
#   channel; WPORG_SVN_USERNAME/PASSWORD (+ WPORG_SLUG/WPORG_SVN_URL vars) for wp.org.
#
# Per-plugin knobs live in mu-release.yaml (all optional; convention defaults):
#   channel:   "ee-extensions/foh-updates"
#   app_build: false            # build app/ (Vite) + gate on assets/app staleness
#   sections:  { autogen: true }
set -euo pipefail

SLUG="$(basename "${GITHUB_REPOSITORY}")"
VER="${GITHUB_REF_NAME#v}"
MAIN="${SLUG}.php"
HOST="${GITHUB_SERVER_URL}"
CFG=".mu-release.yaml"

# --- config helpers (yaml via python; pure-convention defaults if absent) -----
cfg() { python3 - "$1" "$2" <<'PY' 2>/dev/null || true
import sys, os
key, default = sys.argv[1], sys.argv[2]
val = default
if os.path.isfile(".mu-release.yaml"):
    import yaml
    data = yaml.safe_load(open(".mu-release.yaml")) or {}
    cur = data
    for part in key.split("."):
        cur = cur.get(part) if isinstance(cur, dict) else None
        if cur is None:
            cur = default; break
    val = cur
print("true" if val is True else "false" if val is False else val)
PY
}

# Builder/config deps up front: cfg() parses YAML and build-manifest needs
# markdown + pyyaml. Installing here (not mid-pipeline) so CHANNEL/APP_BUILD and
# every later YAML read resolve correctly.
command -v python3 >/dev/null 2>&1 || { sudo apt-get update -qq && sudo apt-get install -y -qq python3; }
python3 -m pip --version >/dev/null 2>&1 || { sudo apt-get update -qq && sudo apt-get install -y -qq python3-pip; }
python3 -m pip install --quiet markdown pyyaml 2>/dev/null \
  || python3 -m pip install --quiet --break-system-packages markdown pyyaml

CHANNEL="$(cfg channel 'ee-extensions/foh-updates')"
APP_BUILD="$(cfg app_build 'false')"

log() { printf '\n\033[1m▸ %s\033[0m\n' "$*"; }

# --- 1. validate header/const/changelog/readme all match the tag --------------
log "Validate ${MAIN} Version, CHANGELOG, readme.txt vs tag v${VER}"
[ -f "${MAIN}" ] || { echo "::error::main plugin file ${MAIN} not found"; exit 1; }
HVER="$(grep -iE '^[[:space:]]*\*?[[:space:]]*Version:' "${MAIN}" | head -1 | sed -E 's/.*[Vv]ersion:[[:space:]]*//' | tr -d '[:space:]\r')"
[ "${HVER}" = "${VER}" ] || { echo "::error::header Version (${HVER}) != tag (${VER})"; exit 1; }
if grep -qE "_VERSION'" "${MAIN}"; then
  grep -qF "_VERSION', '${VER}'" "${MAIN}" || { echo "::error::*_VERSION const != ${VER}"; exit 1; }
fi
# CHANGELOG.md is documentation, not correctness — warn, never block a release
# (the modal's changelog comes from readme.txt's == Changelog == section anyway,
# and version/tag/stable are already enforced hard above).
if [ -f CHANGELOG.md ] && ! grep -qF "[${VER}]" CHANGELOG.md; then
  echo "::warning::CHANGELOG.md has no [${VER}] entry (documentation only — not blocking)."
fi
[ -f readme.txt ] || { echo "::error::no readme.txt — required for packaging"; exit 1; }
RSTABLE="$(grep -iE '^Stable tag:' readme.txt | head -1 | sed -E 's/.*:[[:space:]]*//' | tr -d '[:space:]\r')"
[ "${RSTABLE}" = "${VER}" ] || { echo "::error::readme.txt Stable tag (${RSTABLE}) != tag (${VER})"; exit 1; }
grep -qiE '^License:' readme.txt || { echo "::error::readme.txt missing 'License:' header"; exit 1; }
grep -qE '^== Changelog ==' readme.txt || { echo "::error::readme.txt missing '== Changelog ==' section"; exit 1; }
echo "OK: version/changelog/readme all agree on ${VER}."

# --- 2. optional Vite app build + staleness gate ------------------------------
if [ "${APP_BUILD}" = "true" ] && [ -f app/package.json ]; then
  log "Build app/ (Vite) + gate on assets/app staleness"
  ( cd app && npm ci && npm run build )
  if ! git diff --quiet -- assets/app; then
    echo "::error::assets/app is stale — rebuild, commit, re-tag."
    git --no-pager diff --stat -- assets/app; exit 1
  fi
else
  echo "No app build (app_build=${APP_BUILD})."
fi

# --- 3. package-hygiene scan: nothing stray may ship --------------------------
log "Package hygiene scan"
rm -rf _scan && mkdir _scan
git archive "${GITHUB_REF_NAME}" | tar -x -C _scan
BAD="$(cd _scan && find . \( -iname '*claude*' -o -name '.git' -o -name '.gitea' \
  -o -name '.github' -o -name 'node_modules' -o -name '.DS_Store' -o -name 'Thumbs.db' \
  -o -name '.build-notes-*' -o -name '*.scratch' -o -name 'dev' -o -name 'docs' \) -print)"
[ -z "${BAD}" ] || { echo "::error::disallowed files in package:"; echo "${BAD}"; exit 1; }
if grep -rIilE 'claude|sticky[-_]dvr' _scan; then
  echo "::error::forbidden token in a shipped file"; exit 1
fi
rm -rf _scan
echo "Package hygiene OK."

# --- 4. build the zip (git archive honours .gitattributes export-ignore) ------
log "Package ${SLUG}-${VER}.zip"
ZIP="${SLUG}-${VER}.zip"
git archive --format=zip --prefix="${SLUG}/" -o "${ZIP}" "${GITHUB_REF_NAME}"
ls -l "${ZIP}"

# --- 5. cut the Gitea release + attach the zip --------------------------------
log "Publish Gitea release"
API="${HOST}/api/v1/repos/${GITHUB_REPOSITORY}"
AUTH="Authorization: token ${GITHUB_TOKEN}"
# Idempotent: reuse the release if this tag was already released (re-run/re-tag).
REL="$(curl -fsS -H "${AUTH}" "${API}/releases/tags/${GITHUB_REF_NAME}" 2>/dev/null || true)"
REL_ID="$(printf '%s' "${REL}" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*' || true)"
if [ -z "${REL_ID}" ]; then
  BODY="$(python3 -c 'import json,sys; print(json.dumps({"tag_name":sys.argv[1],"name":sys.argv[2],"body":sys.argv[3]}))' \
          "${GITHUB_REF_NAME}" "${SLUG} ${VER}" "Automated build of ${GITHUB_REF_NAME}.")"
  REL="$(curl -fsS -X POST "${API}/releases" -H "${AUTH}" -H "Content-Type: application/json" --data-raw "${BODY}")"
  REL_ID="$(printf '%s' "${REL}" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*' || true)"
fi
[ -n "${REL_ID}" ] || { echo "::error::could not create/get the release"; echo "${REL}"; exit 1; }
# Replace a same-named asset on a re-run rather than 409-ing.
OLD="$(curl -fsS -H "${AUTH}" "${API}/releases/${REL_ID}/assets" 2>/dev/null \
       | python3 -c "import json,sys;[print(a['id']) for a in (json.load(sys.stdin) or []) if a.get('name')==sys.argv[1]]" "${ZIP}" 2>/dev/null || true)"
[ -z "${OLD}" ] || curl -fsS -X DELETE -H "${AUTH}" "${API}/releases/${REL_ID}/assets/${OLD}" >/dev/null 2>&1 || true
curl -fsS -X POST "${API}/releases/${REL_ID}/assets?name=${ZIP}" -H "${AUTH}" \
  -F "attachment=@${ZIP};type=application/zip" >/dev/null
echo "Released ${GITHUB_REF_NAME}."

# --- 6. publish to the self-hosted update channel (skipped without deploy key) -
if [ -z "${FOH_UPDATES_DEPLOY_KEY:-}" ]; then
  echo "FOH_UPDATES_DEPLOY_KEY not set — skipping channel publish."; exit 0
fi
log "Publish to channel ${CHANNEL}"
SSH_HOST="${HOST#https://}"; SSH_HOST="${SSH_HOST#http://}"
SSH_PORT="${FOH_UPDATES_SSH_PORT:-30009}"
DL="${HOST}/${CHANNEL}/raw/branch/main/${SLUG}/${ZIP}"
RAW="${HOST}/${CHANNEL}/raw/branch/main/${SLUG}"

install -d -m 700 "${HOME}/.ssh"
KEY="${HOME}/.ssh/foh_updates_deploy"; KH="${HOME}/.ssh/known_hosts"
printf '%s\n' "${FOH_UPDATES_DEPLOY_KEY}" > "${KEY}"; chmod 600 "${KEY}"
ssh-keyscan -p "${SSH_PORT}" "${SSH_HOST}" >> "${KH}" 2>/dev/null
export GIT_SSH_COMMAND="ssh -i ${KEY} -o IdentitiesOnly=yes -o UserKnownHostsFile=${KH}"

git clone --depth 1 "ssh://git@${SSH_HOST}:${SSH_PORT}/${CHANNEL}.git" _upd

# computed (release-derived) fields → build-manifest merges them with manifest.json + sections.
NAME="$(grep -iE '^[[:space:]]*\*?[[:space:]]*Plugin Name:' "${MAIN}" | head -1 | sed -E 's/.*Plugin Name:[[:space:]]*//' | tr -d '\r')"
REQ="$(grep -iE '^Requires at least:' readme.txt | head -1 | sed -E 's/.*:[[:space:]]*//' | tr -d '[:space:]\r' || true)"
TESTED="$(grep -iE '^Tested up to:' readme.txt | head -1 | sed -E 's/.*:[[:space:]]*//' | tr -d '[:space:]\r' || true)"
RPHP="$(grep -iE '^Requires PHP:' readme.txt | head -1 | sed -E 's/.*:[[:space:]]*//' | tr -d '[:space:]\r' || true)"
NOW="$(date -u +%Y-%m-%d' '%H:%M:%S)"

ICONS='{}'
if [ -f icons/icon-256x256.png ]; then
  ICONS="$(printf '{"1x":"%s/icon-128x128.png","2x":"%s/icon-256x256.png"}' "${RAW}" "${RAW}")"
fi
# screenshot images (screenshots/screenshot-N.ext) -> {N: filename}; build-manifest
# pairs these with the readme == Screenshots == captions into the `screenshots` field.
SHOTS='{}'
if [ -d screenshots ]; then
  SHOTS="$(ls screenshots 2>/dev/null | python3 -c 'import sys,re,json;print(json.dumps({m.group(1):m.group(0) for m in (re.match(r"screenshot-(\d+)\.(?:png|jpe?g|gif|webp)$",f,re.I) for f in sys.stdin.read().split()) if m}))' || echo '{}')"
fi
python3 - "$NAME" "$SLUG" "$VER" "$DL" "$HOST/${CHANNEL%/*}/$SLUG" "$REQ" "$TESTED" "$RPHP" "$NOW" "$ICONS" "$SHOTS" "$RAW" > computed.json <<'PY'
import json, sys
k = ["name","slug","version","download_url","homepage","requires","tested","requires_php","last_updated"]
d = dict(zip(k, sys.argv[1:10]))
d["icons"] = json.loads(sys.argv[10])
shots = json.loads(sys.argv[11])
if shots:
    raw = sys.argv[12]
    d["_screenshots"] = {n: f"{raw}/{fn}" for n, fn in shots.items()}
json.dump(d, sys.stdout)
PY

# Prefer the tag-locked local builder (MU ships .ci/); consumer plugins fall back
# to the channel copy seeded by MU's release. (Keep FOH_UPDATES_DEPLOY_KEY tightly held.)
BUILDER="_upd/.ci/build-manifest.py"
[ -f .ci/build-manifest.py ] && BUILDER=".ci/build-manifest.py"
python3 "${BUILDER}" --repo-root . --computed computed.json --config "${CFG}" --out "_upd/${SLUG}.json"

mkdir -p "_upd/${SLUG}"
rm -f "_upd/${SLUG}"/*.zip
cp "${ZIP}" "_upd/${SLUG}/"
[ -d icons ] && cp icons/icon-128x128.png icons/icon-256x256.png "_upd/${SLUG}/" 2>/dev/null || true
[ -d screenshots ] && cp screenshots/screenshot-*.* "_upd/${SLUG}/" 2>/dev/null || true

( cd _upd
  git config user.email "ci@${SSH_HOST}"
  git config user.name "ee-extensions release bot"
  git add -A
  if git diff --cached --quiet; then echo "channel already current — nothing to publish."; exit 0; fi
  git commit -m "release ${SLUG} ${VER}"
  pushed=0
  for i in 1 2 3 4 5; do
    if git push; then pushed=1; break; fi
    echo "channel push raced; backing off ${i}/5"
    sleep $(( (RANDOM % 20) + 1 ))   # large random jitter to de-sync concurrent releases
    git pull --rebase --no-edit
  done
  [ "${pushed}" = 1 ] || { echo "::error::channel push failed after 5 retries"; exit 1; }
  echo "Published ${SLUG} ${VER} to ${CHANNEL}: ${DL}" )
