#!/usr/bin/env bash

set -Eeuo pipefail

# bundler-cache: true in setup-ruby enables frozen mode, which blocks
# updating Gemfile.lock after the gemspec version bump. Disable it for
# every bundler invocation in this script so the lockfile can be
# regenerated to match the new version.
export BUNDLE_FROZEN=false
export BUNDLE_DEPLOYMENT=false

release_branch=""

header() {
  echo "==============================="
  echo "$1"
  echo "==============================="
}

fail() {
  echo "Error: $1" >&2
  exit 1
}

on_exit() {
  local status=$?
  [ "$status" == "0" ] && return
  echo "" >&2
  echo "Release failed (exit $status)." >&2
  if [ -n "$release_branch" ]; then
    echo "To reset and try again:" >&2
    echo "  git reset --hard && git clean -fd" >&2
    echo "  git switch main && git branch -D $release_branch" >&2
  fi
}
trap on_exit EXIT

# In-place file edits via perl, since `sed -i` is incompatible between
# GNU (Linux/CI) and BSD (macOS) sed.
inplace() {
  local expression=$1
  shift
  perl -pi -e "$expression" "$@"
}

branch_name() {
  git symbolic-ref --short HEAD
}

working_tree_is_clean() {
  [ -z "$(git status --porcelain)" ]
}

commits_behind_remote() {
  git rev-list --count --left-only @{u}...HEAD
}

commits_ahead_of_remote() {
  git rev-list --count --right-only @{u}...HEAD
}

tag_exists() {
  git rev-parse --quiet --verify "refs/tags/v$1" > /dev/null ||
    [ -n "$(git ls-remote --tags origin "refs/tags/v$1")" ]
}

prerequisite_checks() {
  header "Prerequisite Checks"

  working_tree_is_clean || fail "unclean working tree"
  [ "$(branch_name)" == "main" ] || fail "can only make a release on the main branch"

  git fetch --all --tags --quiet

  [ "$(commits_ahead_of_remote)" == "0" ] ||
    fail "changes exist that haven't been pushed to origin"

  if [ "$(commits_behind_remote)" != "0" ]; then
    echo "Pulling latest changes from origin..."
    git pull --ff-only --quiet
  fi
}

prompt_for_version() {
  echo "Current version is $major.$minor.$patch."
  echo "Type the number of an option to bump, or pick Manual to enter a version number"
  select bump in Major Minor Patch Manual; do
    case "$bump" in
      Major) major=$((major + 1)); minor=0; patch=0 ;;
      Minor) minor=$((minor + 1)); patch=0 ;;
      Patch) patch=$((patch + 1)) ;;
      *)
        read -r -p "What version? (Currently $major.$minor.$patch): " new_version
        [[ "$new_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "version must look like X.Y.Z"
        [ "$new_version" != "$major.$minor.$patch" ] || fail "can't be the same version"
        IFS=. read -r major minor patch <<< "$new_version"
        ;;
    esac
    break
  done
}

update_changelog() {
  VERSION="$1" perl -0777 -pi -e 's/^## main\n/## main\n\n## $ENV{VERSION}\n/m' docs/CHANGELOG.md
  grep -q "^## $1\$" docs/CHANGELOG.md || fail "failed to add $1 heading to docs/CHANGELOG.md"
}

update_ruby_version() {
  inplace "s/MAJOR = \\d+/MAJOR = $1/; s/MINOR = \\d+/MINOR = $2/; s/PATCH = \\d+/PATCH = $3/" \
    lib/view_component/version.rb
  inplace "s/DEPRECATION_HORIZON = \"\\d+\\.\\d+\\.\\d+\"/DEPRECATION_HORIZON = \"$(($1 + 1)).0.0\"/" \
    lib/view_component/deprecation.rb

  grep -q "MAJOR = $1\$" lib/view_component/version.rb &&
    grep -q "MINOR = $2\$" lib/view_component/version.rb &&
    grep -q "PATCH = $3\$" lib/view_component/version.rb ||
    fail "failed to update lib/view_component/version.rb"
}

update_lockfiles() {
  # The only lockfile change during a release is the view_component
  # version pinned by the path gemspec. Running `bundle install`/`bundle
  # exec` locally can add platform-specific entries (e.g. on a dev Ruby
  # without precompiled gems), and `bundle lock` from a mismatched Ruby
  # produces lockfiles CI cannot consume in frozen mode. So restore the
  # committed lockfiles and edit just the version in place.
  local lockfiles=(Gemfile.lock gemfiles/*.gemfile.lock)
  git restore --source=HEAD -- "${lockfiles[@]}"
  VERSION="$1" perl -pi -e 's/^(  |    )view_component \(\d+\.\d+\.\d+\)/$1view_component ($ENV{VERSION})/' "${lockfiles[@]}"

  local lockfile
  for lockfile in "${lockfiles[@]}"; do
    grep -q "view_component ($1)" "$lockfile" || fail "failed to update $lockfile"
  done
}

build_docs() {
  echo "version: $1" > docs/_data/library.yml
  ruby script/sync_contributors.rb
  # Install after the version bump, which changes the resolved gemspec.
  bundle install --quiet
  bundle exec rake docs:build
}

commit() {
  git add \
    docs/api.md \
    docs/CHANGELOG.md \
    docs/_data/contributors.yml \
    docs/_data/library.yml \
    Gemfile.lock \
    gemfiles/*.gemfile.lock \
    lib/view_component/deprecation.rb \
    lib/view_component/version.rb
  git commit --quiet -m "release $1"
}

push() {
  local version=$1

  if [ -n "${RELEASE_DRY_RUN:-}" ]; then
    echo "RELEASE_DRY_RUN set; skipping push and PR creation."
    return
  fi

  # Force-push: the release-X-Y-Z branch is auto-generated and is safe
  # to overwrite. A previous failed run may have left the branch on the
  # remote, which would otherwise reject a non-fast-forward push.
  git push --force origin "$release_branch"

  # Open PR (or reuse an existing one from a previous failed run).
  pr_url=$(gh pr view "$release_branch" \
    --repo ViewComponent/view_component \
    --json url --jq .url 2>/dev/null || true)
  if [ -z "$pr_url" ]; then
    pr_url=$(gh pr create \
      --title "Release $version" \
      --body "Automated release PR for version $version." \
      --base main \
      --head "$release_branch" \
      --repo ViewComponent/view_component)
  else
    echo "Reusing existing PR: $pr_url"
  fi

  echo "####################################################"
  echo "PR opened: $pr_url"
  echo ""
  echo "After merging to main, run:"
  echo "  git checkout main"
  echo "  git pull"
  echo "  script/publish"
  echo ""
  echo "This will:"
  echo "  1. Publish updated docs to gh-pages"
  echo "  2. Create a GitHub release with CHANGELOG entries"
  echo "  3. The gem will be pushed to RubyGems automatically by"
  echo "     Github Actions using Trusted Publishing."
  echo "####################################################"
}

main() {
  prerequisite_checks

  major=$(grep "MAJOR = " lib/view_component/version.rb | grep -o "[0-9]\+")
  minor=$(grep "MINOR = " lib/view_component/version.rb | grep -o "[0-9]\+")
  patch=$(grep "PATCH = " lib/view_component/version.rb | grep -o "[0-9]\+")

  # Arguments are used by the release workflow: MAJOR MINOR PATCH [PRE]
  if [ $# -gt 0 ]; then
    [ $# -ge 3 ] || fail "usage: script/release [MAJOR MINOR PATCH [PRE]]"
    major=$1
    minor=$2
    patch=$3
  else
    prompt_for_version
  fi

  version="$major.$minor.$patch"

  tag_exists "$version" && fail "tag v$version already exists"

  release_branch="release-$major-$minor-$patch"

  header "Creating release for $version"

  git switch --quiet -C "$release_branch"
  update_changelog "$version"
  update_ruby_version "$major" "$minor" "$patch"
  build_docs "$version"
  update_lockfiles "$version"
  commit "$version"
  push "$version"
}

main "$@"
