#!/usr/bin/env bash
#
# Generate (or refresh) the Sparkle appcast for a channel from the DMGs in dist/.
#
#   scripts/generate-appcast.sh {canary|beta|rc|stable}
#
# Sparkle's `generate_appcast` scans a folder of updates (our signed+notarized DMGs),
# writes an appcast XML describing them, and EdDSA-signs each entry with the private
# update key stored in your login keychain (created once by Sparkle's `generate_keys`
# — see signing/README.md). The enclosure (DMG) URLs are rewritten to NUCLEIC_FEED_BASE/<channel>/
# (the R2 host, channel-foldered for tidy download links), so the emitted appcast uploads as-is.
#
# Output: dist/appcast-<channel>.xml  (upload it + the channel's DMG(s) to the R2 bucket
# whose download base is NUCLEIC_FEED_BASE — scripts/upload-r2.sh does this).
#
# Env:
#   NUCLEIC_FEED_BASE   download base for enclosure URLs
#                       (default: https://updates.nucleic.blakeslee.xyz)
#   GENERATE_APPCAST    path to the generate_appcast tool (default: auto-detect)
#   NUCLEIC_APPCAST_MAX_VERSIONS  items to keep per branch in the feed, passed to
#                       generate_appcast --maximum-versions (default 0 = keep all history;
#                       set e.g. 10 to bound the feed + disk, pruning older DMGs to old_updates/)

set -euo pipefail

CHANNEL="${1:?usage: generate-appcast.sh (canary|beta|rc|stable)}"
case "$CHANNEL" in
  canary) SLUG="Nucleic-Canary" ;;
  beta)   SLUG="Nucleic-Beta" ;;
  rc)     SLUG="Nucleic-RC" ;;
  stable) SLUG="Nucleic" ;;
  *) echo "usage: $0 {canary|beta|rc|stable}" >&2; exit 2 ;;
esac

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
FEED_BASE="${NUCLEIC_FEED_BASE:-https://updates.nucleic.blakeslee.xyz}"

# Locate generate_appcast: explicit env, then PATH, then the Sparkle SwiftPM artifact.
GEN="${GENERATE_APPCAST:-}"
[ -z "$GEN" ] && GEN="$(command -v generate_appcast || true)"
if [ -z "$GEN" ]; then
  GEN="$(find "$ROOT/.build" -type f -name generate_appcast -perm -u+x 2>/dev/null | head -1 || true)"
fi
if [ -z "$GEN" ] || [ ! -x "$GEN" ]; then
  cat >&2 <<EOF
generate-appcast: 'generate_appcast' tool not found.

It ships with Sparkle. Get it via either:
  • a normal build that resolves Sparkle, then it's under .build/ (auto-detected), or
  • the Sparkle release tarball / 'brew install --cask sparkle' (then set GENERATE_APPCAST).
EOF
  exit 1
fi

# Publish INCREMENTALLY into a persistent per-channel archives directory. generate_appcast is
# built for exactly this: it reuses an appcast already present in the directory and only ADDS
# entries for archives it hasn't seen, caching extracted metadata in ~/Library/Caches/Sparkle_generate_appcast.
#
# The old flow wiped this directory (rm -rf) and re-copied EVERY dist DMG each run, presenting the
# tool with the whole back-catalogue and NO prior appcast — so it re-mounted (hdiutil attach) and
# re-signed every historical DMG on every release. That's O(all releases): seconds when the catalogue
# was small, minutes once it grew (in lockstep with the R2 bucket). Keeping the directory + appcast and
# staging only genuinely-new DMGs makes each release O(1): it extracts just its own DMG.
STAGE="$ROOT/dist/appcast/$CHANNEL"      # persistent across releases — do NOT wipe (see above)
mkdir -p "$STAGE"
APPCAST_NAME="appcast-$CHANNEL.xml"
OUT="$ROOT/dist/$APPCAST_NAME"

# Seed the archives dir with the most recent appcast so generate_appcast updates it in place instead
# of rebuilding from nothing. Prefer the copy this script wrote last time; on a fresh checkout fall
# back to the live feed on R2 (the authoritative published appcast) so history isn't lost.
if [ ! -f "$STAGE/$APPCAST_NAME" ]; then
  if [ -f "$OUT" ]; then
    cp "$OUT" "$STAGE/$APPCAST_NAME"
  elif command -v curl >/dev/null 2>&1; then
    curl -fsS "$FEED_BASE/$APPCAST_NAME" -o "$STAGE/$APPCAST_NAME" 2>/dev/null || rm -f "$STAGE/$APPCAST_NAME"
  fi
fi

# Stage only DMGs not already represented in the feed (enclosure URLs carry the basename). Newly
# built releases get staged + extracted; the back-catalogue is left untouched — those entries persist
# from the reused appcast without re-mounting a single old DMG. Hardlink when possible (no copy, and a
# stable mtime keeps generate_appcast's cache warm); fall back to cp across filesystems.
shopt -s nullglob
dist_dmgs=("$ROOT/dist/${SLUG}-"*.dmg)
shopt -u nullglob
[ "${#dist_dmgs[@]}" -gt 0 ] || { echo "generate-appcast: no ${SLUG}-*.dmg in dist/ — build a release first" >&2; exit 1; }
staged=0
for dmg in "${dist_dmgs[@]}"; do
  base="$(basename "$dmg")"
  if [ -f "$STAGE/$APPCAST_NAME" ] && grep -qF "$base" "$STAGE/$APPCAST_NAME"; then
    continue   # already in the feed — nothing to (re)process
  fi
  [ -e "$STAGE/$base" ] || ln "$dmg" "$STAGE/$base" 2>/dev/null || cp "$dmg" "$STAGE/$base"
  staged=$((staged + 1))
done

echo "▸ Generating appcast for $CHANNEL ($staged new DMG(s); reusing $STAGE)"
# Enclosure URLs land under the channel folder → updates.nucleic.blakeslee.xyz/<channel>/<dmg>
# (scripts/upload-r2.sh uploads the DMGs to that same key prefix). Trailing slash is required.
# --maximum-versions 0 (default) keeps full history in the feed; a positive value bounds it, moving
# older DMGs to old_updates/ (--auto-prune-update-files then deletes those after 2 weeks).
#
# generate_appcast mounts each *new* DMG to read its version/build metadata, and Sparkle's compiled
# tool still does that through the deprecated `hdiutil attach -mountpoint -nobrowse` path — printing a
# cosmetic "…is deprecated, use diskutil image attach…" warning we can't silence at the source (it
# lives inside the Sparkle binary; fixing it is upstream's job). Filter just those hdiutil
# deprecation lines out of its stderr; all other output and the tool's exit status pass through
# unchanged. fd 3 carries stdout past the grep; `|| true` stops grep-filters-everything from
# tripping pipefail, while a real generate_appcast failure still propagates (pipefail).
{ "$GEN" \
    --download-url-prefix "$FEED_BASE/$CHANNEL/" \
    --maximum-versions "${NUCLEIC_APPCAST_MAX_VERSIONS:-0}" \
    --auto-prune-update-files \
    -o "$STAGE/$APPCAST_NAME" \
    "$STAGE" 2>&1 1>&3 \
    | { grep -vE '^hdiutil: WARNING:.*deprecated' >&2 || true; } ; } 3>&1

# -o pins the output name, so it's deterministic; guard anyway.
GENERATED="$STAGE/$APPCAST_NAME"
[ -f "$GENERATED" ] || {
  echo "generate-appcast: generate_appcast produced no appcast XML at $GENERATED" >&2; exit 1; }
cp "$GENERATED" "$OUT"
echo "✓ $OUT"
echo "  Upload to the R2 host '$FEED_BASE' (scripts/upload-r2.sh $CHANNEL):"
echo "    • $(basename "$OUT")            → $FEED_BASE/appcast-$CHANNEL.xml"
echo "    • each ${SLUG}-*.dmg referenced → $FEED_BASE/$CHANNEL/${SLUG}-<ver>.dmg"
echo "  (the embedded SUFeedURL points the app at $FEED_BASE/appcast-$CHANNEL.xml)"
