#!/usr/bin/env bash
# Cut a new Franklin release.
#
# Default flow (branch-protection-friendly):
#   bin/release 0.3.0          # bump, commit on release/0.3.0, open PR
#   (merge the PR)
#   bin/release 0.3.0 --tag    # tag the merge commit, push tag
#
# The tag push triggers .github/workflows/release.yml which creates the
# GitHub Release; that fires publish.yml (PyPI) and homebrew-bump.yml
# (tap PR) automatically. See docs/releasing.md for the full flow.
#
# Flags:
#   --tag       Tag and push (phase 2, after PR merge)
#   --direct    Push to main directly (skip PR, needs admin bypass)
#   --yes       Skip confirmation prompts
#   --dry       Do everything except commit, tag, push

set -euo pipefail

# ---- args -----------------------------------------------------------------

VERSION="${1:-}"
MODE="interactive"
ACTION="pr"

for arg in "${@:2}"; do
    case "$arg" in
        --yes|-y) MODE="yes" ;;
        --dry|-n) MODE="dry" ;;
        --tag|-t) ACTION="tag" ;;
        --direct) ACTION="direct" ;;
        *) echo "error: unknown flag '$arg'" >&2; exit 2 ;;
    esac
done

if [[ -z "$VERSION" ]]; then
    cat >&2 <<EOF
usage: bin/release X.Y.Z [--tag|--direct|--dry|--yes]

  X.Y.Z     semver version (e.g. 0.3.0, 1.2.4)
  --tag     phase 2: tag the merge commit after the release PR is merged
  --direct  push to main directly (needs admin bypass on protected branches)
  --dry     bump and cut, but don't commit, tag, or push
  --yes     skip confirmation prompts

Default flow (works with branch protection):
  bin/release 0.3.0          # bump, open PR
  (merge the PR on GitHub)
  bin/release 0.3.0 --tag    # tag merge commit, trigger publish chain

See docs/releasing.md for the end-to-end release flow.
EOF
    exit 2
fi

if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "error: version must be plain X.Y.Z (got '$VERSION')" >&2
    exit 2
fi

REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"

# ---- phase 2: tag after merge --------------------------------------------

if [[ "$ACTION" == "tag" ]]; then
    git checkout main
    git pull --ff-only

    PYPROJECT_VERSION="$(grep '^version = ' pyproject.toml | head -1 | cut -d'"' -f2)"
    if [[ "$PYPROJECT_VERSION" != "$VERSION" ]]; then
        echo "error: pyproject.toml has version $PYPROJECT_VERSION, expected $VERSION" >&2
        echo "  is the release PR merged?" >&2
        exit 1
    fi

    if git rev-parse "v$VERSION" >/dev/null 2>&1; then
        echo "error: tag v$VERSION already exists locally" >&2
        exit 1
    fi
    if git ls-remote --tags --exit-code origin "refs/tags/v$VERSION" >/dev/null 2>&1; then
        echo "error: tag v$VERSION already exists on origin" >&2
        exit 1
    fi

    git tag "v$VERSION"

    if [[ "$MODE" == "dry" ]]; then
        echo "--dry: tag v$VERSION created locally but not pushed."
        exit 0
    fi

    git push origin "v$VERSION"

    cat <<EOF

✓ Tagged v$VERSION on $(git rev-parse --short HEAD).

What happens next (watch with \`gh run watch\`):
  1. release.yml creates the GitHub Release from CHANGELOG notes
  2. publish.yml builds + uploads the sdist/wheel to PyPI
  3. homebrew-bump.yml waits for PyPI, opens a PR on the tap repo
EOF
    exit 0
fi

# ---- phase 1: preflight --------------------------------------------------

if ! git diff --quiet || ! git diff --cached --quiet; then
    echo "error: working tree is dirty — commit or stash first" >&2
    exit 1
fi

CURRENT_BRANCH="$(git branch --show-current)"
if [[ "$CURRENT_BRANCH" != "main" ]]; then
    echo "error: must be on main (currently on '$CURRENT_BRANCH')" >&2
    exit 1
fi

git fetch --quiet origin
LOCAL="$(git rev-parse @)"
REMOTE="$(git rev-parse '@{u}')"
if [[ "$LOCAL" != "$REMOTE" ]]; then
    echo "error: main is not in sync with origin/main — pull or push first" >&2
    exit 1
fi

CURRENT_VERSION="$(grep '^version = ' pyproject.toml | head -1 | cut -d'"' -f2)"
if [[ "$VERSION" == "$CURRENT_VERSION" ]]; then
    echo "error: $VERSION is already the current version in pyproject.toml" >&2
    exit 1
fi

if git rev-parse "v$VERSION" >/dev/null 2>&1; then
    echo "error: tag v$VERSION already exists locally" >&2
    exit 1
fi

if git ls-remote --tags --exit-code origin "refs/tags/v$VERSION" >/dev/null 2>&1; then
    echo "error: tag v$VERSION already exists on origin" >&2
    exit 1
fi

UNRELEASED_BODY="$(
    awk '
        /^## \[Unreleased\]/ { in_section = 1; next }
        /^## \[/ { in_section = 0 }
        in_section { print }
    ' CHANGELOG.md | sed -e 's/^[[:space:]]*//' -e '/^$/d' || true
)"

if [[ -z "$UNRELEASED_BODY" ]]; then
    echo "error: CHANGELOG.md [Unreleased] section is empty — nothing to release" >&2
    exit 1
fi

DATE="$(date +%Y-%m-%d)"

echo
echo "  current version: $CURRENT_VERSION"
echo "  new version:     $VERSION"
echo "  date:            $DATE"
echo "  mode:            $MODE ($ACTION)"
echo

# ---- bump version ---------------------------------------------------------

sed_inplace() {
    if sed --version >/dev/null 2>&1; then
        sed -i "$@"
    else
        sed -i '' "$@"
    fi
}

sed_inplace "s/^version = \"$CURRENT_VERSION\"$/version = \"$VERSION\"/" pyproject.toml
sed_inplace "s/^__version__ = \"$CURRENT_VERSION\"$/__version__ = \"$VERSION\"/" src/franklin/__init__.py

if ! grep -q "^version = \"$VERSION\"$" pyproject.toml; then
    echo "error: pyproject.toml bump didn't take" >&2
    exit 1
fi
if ! grep -q "^__version__ = \"$VERSION\"$" src/franklin/__init__.py; then
    echo "error: __init__.py bump didn't take" >&2
    exit 1
fi

uv lock --quiet

# ---- cut CHANGELOG --------------------------------------------------------

python3 - "$VERSION" "$DATE" <<'PY'
import re
import sys
from pathlib import Path

version, date = sys.argv[1], sys.argv[2]
path = Path("CHANGELOG.md")
text = path.read_text()

if "## [Unreleased]" not in text:
    print("error: CHANGELOG.md missing [Unreleased] heading", file=sys.stderr)
    sys.exit(1)

text = text.replace(
    "## [Unreleased]\n",
    f"## [Unreleased]\n\n## [{version}] - {date}\n",
    1,
)

unreleased_link_re = re.compile(
    r"\[Unreleased\]: https://github\.com/mcrundo/franklin/compare/v[0-9.]+\.\.\.HEAD"
)
new_unreleased = f"[Unreleased]: https://github.com/mcrundo/franklin/compare/v{version}...HEAD"
if not unreleased_link_re.search(text):
    print("error: CHANGELOG.md missing [Unreleased] link reference", file=sys.stderr)
    sys.exit(1)
text = unreleased_link_re.sub(new_unreleased, text)

new_version_link = f"[{version}]: https://github.com/mcrundo/franklin/releases/tag/v{version}"
if new_version_link not in text:
    text = text.replace(
        new_unreleased + "\n",
        new_unreleased + "\n" + new_version_link + "\n",
        1,
    )

path.write_text(text)
PY

# ---- sanity gate ----------------------------------------------------------

echo "running sanity gate (ruff + mypy + pytest)..."
uv run ruff check . >/dev/null
uv run ruff format --check . >/dev/null
uv run mypy >/dev/null
uv run pytest -q >/dev/null

echo "✓ sanity gate passed"
echo

# ---- commit ---------------------------------------------------------------

if [[ "$MODE" == "dry" ]]; then
    echo "--dry: skipping commit and push. Inspect changes with \`git diff\`."
    exit 0
fi

git add pyproject.toml src/franklin/__init__.py uv.lock CHANGELOG.md
git commit -m "Release $VERSION"

echo "✓ committed Release $VERSION"
echo

# ---- push (branch or direct) ---------------------------------------------

if [[ "$ACTION" == "direct" ]]; then
    git tag "v$VERSION"

    if [[ "$MODE" == "interactive" ]]; then
        read -r -p "Push main + v$VERSION to origin now? [y/N] " confirm
        if [[ ! "$confirm" =~ ^[yY]$ ]]; then
            cat <<EOF

Skipping push. To release later:
    git push origin main && git push origin v$VERSION

To undo:
    git tag -d v$VERSION && git reset --hard HEAD~1
EOF
            exit 0
        fi
    fi

    git push origin main
    git push origin "v$VERSION"

    cat <<EOF

✓ Released v$VERSION.

What happens next (watch with \`gh run watch\`):
  1. release.yml creates the GitHub Release from CHANGELOG notes
  2. publish.yml builds + uploads the sdist/wheel to PyPI
  3. homebrew-bump.yml waits for PyPI, opens a PR on the tap repo
EOF

else
    # Default: create a release branch and open a PR.
    BRANCH="release/$VERSION"
    git checkout -b "$BRANCH"
    git push -u origin "$BRANCH"

    if command -v gh >/dev/null 2>&1; then
        gh pr create \
            --title "Release $VERSION" \
            --body "Version bump, changelog cut, lockfile refresh. Generated by \`bin/release $VERSION\`.

After merge, run:
\`\`\`
bin/release $VERSION --tag
\`\`\`"
        echo
    fi

    # Return to main and clean up the local release commit so main
    # stays in sync with origin/main.
    git checkout main
    git reset --hard origin/main

    cat <<EOF

✓ Release PR opened for v$VERSION.

Next steps:
  1. Merge the PR on GitHub
  2. bin/release $VERSION --tag
EOF
fi
