#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026-present Xquik <support@xquik.com>
#
# SPDX-License-Identifier: Apache-2.0
#
# Publishes 1 version to PyPI from a maintainer's Mac, then pushes its
# v<version> tag and creates the GitHub release.
# Usage: bin/release-local [--dry-run] <version>
# The package takes its version from the Git tag, so the version is an argument.
# PYPI_KEYCHAIN_SERVICE names the macOS Keychain item that holds the PyPI token.
# --dry-run runs every step except the upload, the tag, and the GitHub release.
# It never reads the Keychain, so it needs no PYPI_KEYCHAIN_SERVICE. It also
# runs on any clean commit, so a preview can run before a change lands.
set -euo pipefail
cd "$(dirname "$0")/.."

dry_run=false
version=""
for argument in "$@"; do
  case "$argument" in
    --dry-run) dry_run=true ;;
    -*)
      echo "release-local: unknown option $argument. Use --dry-run and a version such as 1.2.3." >&2
      exit 2
      ;;
    *)
      if [[ -n "$version" ]]; then
        echo "release-local: pass 1 version, such as 1.2.3." >&2
        exit 2
      fi
      version="$argument"
      ;;
  esac
done
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  echo "release-local: pass the version to release, such as 1.2.3." >&2
  exit 2
fi

repository="Xquik-dev/xquik-haystack"
package="xquik-haystack"
tag="v$version"
keychain_service="${PYPI_KEYCHAIN_SERVICE:-}"

step() { echo "==> $1"; }
fail() {
  echo "release-local: $1" >&2
  exit 1
}

# Reads the PyPI token from the Keychain item named by PYPI_KEYCHAIN_SERVICE.
# Messages never name the item, because release output gets pasted in public.
read_token() {
  security find-generic-password -s "$keychain_service" "$@" 2>/dev/null ||
    fail "The Keychain has no item named by PYPI_KEYCHAIN_SERVICE. Add the PyPI token under that name, then run bin/release-local again."
}

step "Checking tools and the GitHub sign-in."
for tool in git uv gh curl shasum security; do
  command -v "$tool" >/dev/null || fail "Install $tool, then rerun."
done
gh auth status >/dev/null 2>&1 || fail "Sign in with gh auth login, then rerun."
if $dry_run; then
  step "This is a dry run, so the PyPI token is not read from the Keychain."
else
  step "Checking the Keychain item named by PYPI_KEYCHAIN_SERVICE."
  [[ -n "$keychain_service" ]] ||
    fail "PYPI_KEYCHAIN_SERVICE is unset. Set it to the Keychain item that holds the PyPI token, then run bin/release-local again."
  read_token >/dev/null
fi

step "Checking for a clean tree."
[[ "$(git remote get-url origin)" == *"$repository"* ]] || fail "Point origin at $repository, then rerun."
[[ -z "$(git status --porcelain)" ]] || fail "Commit or remove local changes, then rerun."
revision="$(git rev-parse HEAD)"
if $dry_run; then
  step "This is a dry run, so the branch is not compared with origin/main."
else
  step "Checking that main equals origin/main."
  [[ "$(git symbolic-ref --quiet --short HEAD || true)" == main ]] || fail "Switch to main, then rerun."
  git fetch --quiet origin main
  [[ "$revision" == "$(git rev-parse origin/main)" ]] || fail "Make main equal to origin/main, then rerun."
fi

step "Checking that origin has no $tag tag."
tag_status=0
git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null || tag_status=$?
case "$tag_status" in
  0) fail "Tag $tag already exists on origin. Pass the next version, then rerun." ;;
  2) ;;
  *) fail "Could not read tags from origin. Check GitHub access, then rerun." ;;
esac
if git rev-parse --quiet --verify "refs/tags/$tag" >/dev/null; then
  fail "Local tag $tag is not on origin. Delete it with git tag -d $tag, then rerun."
fi

step "Running bin/check-local."
bin/check-local
[[ -z "$(git status --porcelain)" && "$(git rev-parse HEAD)" == "$revision" ]] ||
  fail "The checks changed the tree. Review and commit the changes, then rerun."

artifacts="$PWD/dist/releases/$revision"
files=("$artifacts/xquik_haystack-$version-py3-none-any.whl" "$artifacts/xquik_haystack-$version.tar.gz")
step "Building the wheel and source archive in dist/releases/$revision."
SETUPTOOLS_SCM_PRETEND_VERSION="$version" uv tool run --from hatch==1.18.0 hatch build "$artifacts"
built=("$artifacts"/*)
[[ "${#built[@]}" -eq 2 && -f "${files[0]}" && -f "${files[1]}" ]] ||
  fail "dist/releases/$revision holds other files. Move them out, then rerun."

step "Checking package metadata with twine."
uv tool run --from twine==7.0.0 twine check "${files[@]}"

# Succeeds when PyPI lists the SHA-256 digest of both local files.
pypi_matches_build() {
  local metadata file digest
  metadata="$(curl --silent --fail --max-time 30 "https://pypi.org/pypi/$package/$version/json")" || return 1
  for file in "${files[@]}"; do
    digest="$(shasum -a 256 "$file" | cut -d ' ' -f 1)" || return 1
    grep -Eq "\"sha256\": *\"$digest\"" <<<"$metadata" || return 1
  done
}

step "Checking whether PyPI already has $package $version."
pypi_status="$(curl --silent --output /dev/null --write-out '%{http_code}' --max-time 30 "https://pypi.org/pypi/$package/$version/json")" ||
  fail "Could not reach PyPI. Check network access, then rerun."
on_pypi=true
case "$pypi_status" in
  200)
    step "PyPI already has $package $version, so the upload is skipped."
    ;;
  404)
    if $dry_run; then
      on_pypi=false
      step "PyPI has no $package $version yet. The dry run skips the upload."
    else
      step "Uploading to PyPI with the token from the Keychain item named by PYPI_KEYCHAIN_SERVICE."
      (
        token="$(read_token -w)"
        UV_PUBLISH_TOKEN="$token" uv publish \
          --publish-url https://upload.pypi.org/legacy/ \
          --check-url https://pypi.org/simple/ \
          "${files[@]}"
      )
    fi
    ;;
  *)
    fail "PyPI returned HTTP $pypi_status. Retry later."
    ;;
esac

if $on_pypi; then
  step "Comparing the PyPI files with the local build."
  for attempt in 1 2 3 4 5; do
    if pypi_matches_build; then
      break
    fi
    ((attempt < 5)) || fail "PyPI files differ from this build or are not visible yet. Compare them before tagging."
    step "PyPI is not ready yet. Retrying in ${RELEASE_RETRY_SECONDS:-30} seconds."
    sleep "${RELEASE_RETRY_SECONDS:-30}"
  done
else
  step "The dry run skips the comparison with PyPI, because nothing was uploaded."
fi

if $dry_run; then
  step "The dry run skips tag $tag and the GitHub release."
  echo "Dry run finished for $package $version. Nothing was uploaded, tagged, or released."
  exit 0
fi

step "Creating annotated tag $tag at $revision and pushing it to origin."
git tag -a "$tag" -m "Release $version" "$revision"
git push origin "refs/tags/$tag"

step "Creating GitHub release $tag with generated notes."
gh release create "$tag" --repo "$repository" --verify-tag --title "$tag" --generate-notes ||
  fail "Tag $tag is on origin, but the GitHub release failed. Create it with gh release create $tag --verify-tag."

echo "Released $package $version as $tag."
