#!/usr/bin/env bash
# publish-banger-release.sh
#
# Cut and publish a banger release tarball + SHA256SUMS + cosign
# signature to the R2 bucket consumed by `banger update`.
#
# Usage:
#   scripts/publish-banger-release.sh v0.1.0
#
# Environment overrides:
#   COSIGN_KEY       path to the cosign private key  (default: cosign.key)
#   RCLONE_REMOTE    rclone remote name              (default: releases)
#   RCLONE_BUCKET    R2 bucket name (rclone target)  (default: releases)
#   BUCKET_PATH      object-key prefix in the bucket (default: banger)
#   BASE_URL         public URL prefix for objects   (default: https://releases.thaloco.com)
#   SKIP_UPLOAD      set to 1 to stage everything locally without rclone upload
#
# rclone path layout:
#   ${RCLONE_REMOTE}:${RCLONE_BUCKET}/${BUCKET_PATH}/...
# i.e. defaults resolve to releases:releases/banger/v0.1.0/<file>.
# Public URLs in the manifest are ${BASE_URL}/${BUCKET_PATH}/<version>/<file>
# (BASE_URL is the bucket's public custom domain, so the bucket name
# itself is implicit there).
#
# Prerequisites:
#   * cosign in PATH  (https://github.com/sigstore/cosign)
#   * rclone in PATH, configured with a remote named ${RCLONE_REMOTE}
#     that targets the R2 account hosting ${RCLONE_BUCKET}, which is
#     publicly served at ${BASE_URL}.
#   * A cosign keypair already generated. The public key MUST already
#     be embedded in internal/updater/verify_signature.go's
#     BangerReleasePublicKey constant — running this script with a
#     placeholder key would publish a release no installed banger can
#     verify.
#
# Output (under build/release/<version>/):
#   banger-<version>-linux-amd64.tar.gz
#   SHA256SUMS
#   SHA256SUMS.sig
#   manifest.json     (the freshly-mutated copy uploaded to the bucket)

set -euo pipefail

log() { printf '[publish-banger-release] %s\n' "$*" >&2; }
die() { log "$*"; exit 1; }

if [[ $# -lt 1 ]]; then
    die "usage: $0 <version>  (e.g. $0 v0.1.0)"
fi

VERSION="$1"
case "$VERSION" in
    v*.*.*) ;;
    *) die "version must look like vX.Y.Z, got $VERSION" ;;
esac

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

COSIGN_KEY="${COSIGN_KEY:-cosign.key}"
RCLONE_REMOTE="${RCLONE_REMOTE:-releases}"
RCLONE_BUCKET="${RCLONE_BUCKET:-releases}"
BUCKET_PATH="${BUCKET_PATH:-banger}"
BASE_URL="${BASE_URL:-https://releases.thaloco.com}"
SKIP_UPLOAD="${SKIP_UPLOAD:-0}"

RCLONE_DEST_BASE="$RCLONE_REMOTE:$RCLONE_BUCKET/$BUCKET_PATH"

command -v cosign >/dev/null || die "cosign not in PATH"
command -v rclone >/dev/null || die "rclone not in PATH"
command -v sha256sum >/dev/null || die "sha256sum not in PATH"
command -v jq >/dev/null || die "jq not in PATH"

[[ -f "$COSIGN_KEY" ]] || die "cosign key not found at $COSIGN_KEY (override with COSIGN_KEY=...)"

cd "$REPO_ROOT"

OUT_DIR="$REPO_ROOT/build/release/$VERSION"
TARBALL_NAME="banger-$VERSION-linux-amd64.tar.gz"
TARBALL_PATH="$OUT_DIR/$TARBALL_NAME"

log "preparing $OUT_DIR"
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR"

log "building binaries with version=$VERSION"
COMMIT="$(git rev-parse HEAD)"
BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
LDFLAGS="-X banger/internal/buildinfo.Version=$VERSION \
         -X banger/internal/buildinfo.Commit=$COMMIT \
         -X banger/internal/buildinfo.BuiltAt=$BUILT_AT"

BUILD_STAGE="$OUT_DIR/stage"
mkdir -p "$BUILD_STAGE"
go build -ldflags "$LDFLAGS" -o "$BUILD_STAGE/banger" ./cmd/banger
go build -ldflags "$LDFLAGS" -o "$BUILD_STAGE/bangerd" ./cmd/bangerd
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags "$LDFLAGS" -o "$BUILD_STAGE/banger-vsock-agent" \
    ./cmd/banger-vsock-agent

log "tarring → $TARBALL_PATH"
# -C into the stage dir so the tarball's root entries are bare
# basenames (banger, bangerd, banger-vsock-agent) — the updater's
# StageTarball validator rejects anything else.
tar -czf "$TARBALL_PATH" -C "$BUILD_STAGE" \
    banger bangerd banger-vsock-agent

log "computing SHA256SUMS"
(
    cd "$OUT_DIR"
    sha256sum "$TARBALL_NAME" > SHA256SUMS
    cat SHA256SUMS
) >&2

log "cosign sign-blob → SHA256SUMS.sig"
# Flag rationale (cosign v3.x):
#   --use-signing-config=false  bypasses the new signing-config flow that
#                               otherwise insists on bundle output + Rekor.
#   --tlog-upload=false         skip the public transparency log; banger's
#                               trust model is "embedded public key", not
#                               "Rekor lookup", so the log adds nothing.
#   --new-bundle-format=false   emit a bare base64 ASN.1 DER signature,
#                               which is what internal/updater consumes
#                               via crypto/ecdsa.VerifyASN1.
# These flags also work on cosign v2.x, so the script is forward- and
# backward-compatible across the v2→v3 boundary.
# If COSIGN_PASSWORD is set in the environment, cosign uses it.
# Otherwise cosign prompts on the terminal — which is what we want
# for a password-protected offline key. Don't pre-set it to empty:
# that suppresses the prompt and makes cosign try to decrypt with
# the empty password, failing with "decryption failed".
cosign sign-blob --yes \
    --key "$COSIGN_KEY" \
    --use-signing-config=false \
    --tlog-upload=false \
    --new-bundle-format=false \
    --output-signature "$OUT_DIR/SHA256SUMS.sig" \
    "$OUT_DIR/SHA256SUMS"

log "verifying signature against the embedded public key"
EMBEDDED_PUB="$OUT_DIR/embedded-pubkey.pem"
# verify_signature.go embeds the PEM inside a Go raw-string literal, so the
# BEGIN line is prefixed with `var ... = ` + backtick and the END line has a
# trailing backtick. Strip those so the result is a clean PEM.
sed -n '/-----BEGIN PUBLIC KEY-----/,/-----END PUBLIC KEY-----/p' \
    "$REPO_ROOT/internal/updater/verify_signature.go" \
  | sed -E 's/.*(-----BEGIN PUBLIC KEY-----)/\1/; s/(-----END PUBLIC KEY-----).*/\1/' \
  > "$EMBEDDED_PUB"
if grep -q PLACEHOLDER "$EMBEDDED_PUB"; then
    die "BangerReleasePublicKey is the placeholder in verify_signature.go; replace it with cosign.pub before publishing"
fi
cosign verify-blob \
    --key "$EMBEDDED_PUB" \
    --insecure-ignore-tlog \
    --signature "$OUT_DIR/SHA256SUMS.sig" \
    "$OUT_DIR/SHA256SUMS"

# install.sh embeds its own copy of the public key for end-user
# verification (curl|bash trust path). Make sure the two copies didn't
# drift; a release with mismatched keys would either reject all
# `banger update` calls or all `install.sh | bash` runs.
log "checking install.sh embedded key matches verify_signature.go"
INSTALL_PUB="$OUT_DIR/install-script-pubkey.pem"
sed -n "/-----BEGIN PUBLIC KEY-----/,/-----END PUBLIC KEY-----/p" \
    "$REPO_ROOT/scripts/install.sh" \
  | sed -E "s/.*(-----BEGIN PUBLIC KEY-----)/\\1/; s/(-----END PUBLIC KEY-----).*/\\1/" \
  > "$INSTALL_PUB"
diff -q "$EMBEDDED_PUB" "$INSTALL_PUB" >/dev/null \
    || die "scripts/install.sh embedded key differs from internal/updater/verify_signature.go; sync them before publishing"

# Build the manifest. Pull the existing manifest from the bucket so
# we don't lose previous release entries, append this one, bump
# latest_stable, write back.
log "fetching existing manifest"
PREV_MANIFEST="$OUT_DIR/manifest.previous.json"
if curl -fsSL "$BASE_URL/$BUCKET_PATH/manifest.json" -o "$PREV_MANIFEST" 2>/dev/null; then
    log "  found previous manifest"
else
    log "  no previous manifest (first release); seeding"
    printf '{"schema_version":1,"latest_stable":"","releases":[]}' > "$PREV_MANIFEST"
fi

NEW_MANIFEST="$OUT_DIR/manifest.json"
RELEASED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
jq --arg version "$VERSION" \
   --arg tarball_url "$BASE_URL/$BUCKET_PATH/$VERSION/$TARBALL_NAME" \
   --arg sums_url "$BASE_URL/$BUCKET_PATH/$VERSION/SHA256SUMS" \
   --arg sig_url "$BASE_URL/$BUCKET_PATH/$VERSION/SHA256SUMS.sig" \
   --arg released_at "$RELEASED_AT" \
   '
   .schema_version = 1
   | .latest_stable = $version
   | .releases = (
       (.releases // [])
       | map(select(.version != $version))
       | . + [{
           "version": $version,
           "tarball_url": $tarball_url,
           "sha256sums_url": $sums_url,
           "sha256sums_sig_url": $sig_url,
           "released_at": $released_at
         }]
     )
   ' "$PREV_MANIFEST" > "$NEW_MANIFEST"

log "manifest:"
jq '.' "$NEW_MANIFEST" >&2

if [[ "$SKIP_UPLOAD" == "1" ]]; then
    log "SKIP_UPLOAD=1, not uploading. Artifacts staged under $OUT_DIR"
    exit 0
fi

log "uploading to $RCLONE_DEST_BASE/$VERSION/"
rclone copy "$TARBALL_PATH"            "$RCLONE_DEST_BASE/$VERSION/"
rclone copy "$OUT_DIR/SHA256SUMS"      "$RCLONE_DEST_BASE/$VERSION/"
rclone copy "$OUT_DIR/SHA256SUMS.sig"  "$RCLONE_DEST_BASE/$VERSION/"

log "uploading manifest"
rclone copy "$NEW_MANIFEST" "$RCLONE_DEST_BASE/"

# install.sh lives at the bucket root (unversioned) so the
# `curl ... install.sh | bash` URL stays stable across releases. The
# script reads manifest.json to find the current latest_stable, so as
# long as install.sh's logic doesn't break, it keeps working for older
# releases too.
log "uploading install.sh"
rclone copy "$REPO_ROOT/scripts/install.sh" "$RCLONE_DEST_BASE/"

log "done. verify with:"
log "  curl -fsSL $BASE_URL/$BUCKET_PATH/manifest.json | jq ."
log "  curl -fsSL $BASE_URL/$BUCKET_PATH/install.sh | head -20"
log "  banger update --check"
