#!/usr/bin/env bash
# Bump unifier-cli version, commit that bump alone, then publish to crates.io.
#
# Usage (from unifier/):
#   ./scripts/release.sh              # patch bump (0.2.0 → 0.2.1)
#   ./scripts/release.sh --minor      # minor bump (0.2.0 → 0.3.0)
#   ./scripts/release.sh --major      # major bump (0.2.0 → 1.0.0)
#   ./scripts/release.sh --set 1.2.3  # exact version
#   ./scripts/release.sh --dry-run    # show what would happen
#   ./scripts/release.sh --skip-tests # skip cargo test before publish
#   ./scripts/release.sh --resume     # commit/publish an already-bumped working tree
#
# Env:
#   RELEASE_NOTES  Optional one-line notes for CHANGELOG (default: "Release $NEW")
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "$REPO_ROOT" ]]; then
  echo "error: not inside a git repository" >&2
  exit 1
fi

BUMP="patch"
SET_VERSION=""
DRY_RUN=false
SKIP_TESTS=false
RESUME=false

usage() {
  cat <<'EOF'
Bump unifier-cli version, commit that bump alone, then publish to crates.io.

Usage (from unifier/):
  ./scripts/release.sh              # patch bump (0.2.0 → 0.2.1)
  ./scripts/release.sh --minor      # minor bump (0.2.0 → 0.3.0)
  ./scripts/release.sh --major      # major bump (0.2.0 → 1.0.0)
  ./scripts/release.sh --set 1.2.3  # exact version
  ./scripts/release.sh --dry-run    # show what would happen
  ./scripts/release.sh --skip-tests # skip cargo test before publish
  ./scripts/release.sh --resume     # finish after an aborted run (commit existing bump)

Flow:
  1. Bump Cargo.toml + CHANGELOG.md; sync Cargo.lock via cargo check
  2. Interactively set/confirm the commit message (Enter keeps the default)
  3. Commit only those version files (Enter = yes)
  4. cargo test (unless --skip-tests); re-check Cargo.lock is clean
  5. mdo publish (cargo publish) (Enter = yes)

Env:
  RELEASE_NOTES  Optional one-line notes for CHANGELOG (default: "Release $NEW")

Also available as:
  mdo release
  mdo release-dry-run
  mdo release-minor
  mdo release-major
EOF
  exit "${1:-0}"
}

# Talk to the real terminal so prompts show immediately even if stdout/stderr
# are piped (e.g. older monorepo runners that line-buffer child output).
tty_printf() {
  if [[ -w /dev/tty ]]; then
    printf '%s' "$*" >/dev/tty
  else
    printf '%s' "$*" >&2
  fi
}

tty_read() {
  # $1 = variable name
  if [[ -r /dev/tty ]]; then
    read -r "$1" </dev/tty || true
  else
    read -r "$1" || true
  fi
}

ask_yes() {
  # $1 = prompt; empty Enter = yes
  local prompt="$1"
  local reply
  tty_printf "$prompt [Y/n] "
  tty_read reply
  case "${reply:-y}" in
    y|Y|yes|YES|"") return 0 ;;
    *) return 1 ;;
  esac
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --patch) BUMP="patch"; shift ;;
    --minor) BUMP="minor"; shift ;;
    --major) BUMP="major"; shift ;;
    --set)
      SET_VERSION="${2:?--set requires VERSION}"
      shift 2
      ;;
    --dry-run) DRY_RUN=true; shift ;;
    --skip-tests) SKIP_TESTS=true; shift ;;
    --resume) RESUME=true; shift ;;
    -h|--help) usage 0 ;;
    *)
      echo "Unknown argument: $1" >&2
      usage 1
      ;;
  esac
done

if [[ ! -f Cargo.toml ]]; then
  echo "error: Cargo.toml not found in $ROOT" >&2
  exit 1
fi

if [[ "$DRY_RUN" != true ]]; then
  if [[ ! -r /dev/tty && ! -t 0 ]]; then
    echo "error: no interactive terminal; refuse to release without confirmation" >&2
    echo "hint: run ./scripts/release.sh from a terminal, or pass --dry-run" >&2
    exit 1
  fi
fi

parse_version_text() {
  sed -n 's/^version = "\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)"$/\1/p' | head -1
}

parse_version_file() {
  parse_version_text <"$1"
}

OLD_HEAD="$(
  cd "$REPO_ROOT"
  git show HEAD:unifier/Cargo.toml 2>/dev/null | parse_version_text || true
)"
WORKING="$(parse_version_file Cargo.toml)"
if [[ -z "$WORKING" ]]; then
  echo "error: could not parse version from Cargo.toml" >&2
  exit 1
fi

VERSION_FILES=(
  "unifier/Cargo.toml"
  "unifier/Cargo.lock"
  "unifier/CHANGELOG.md"
)

can_amend_head() {
  # True if HEAD looks like our release commit and is not already on the remote tip.
  local head_subj
  head_subj="$(cd "$REPO_ROOT" && git log -1 --pretty=%s)"
  if [[ "$head_subj" != "Release unifier-cli $NEW" && "$head_subj" != "${COMMIT_MSG:-}" ]]; then
    return 1
  fi
  (
    cd "$REPO_ROOT"
    if git rev-parse @{u} >/dev/null 2>&1; then
      if [[ "$(git rev-parse HEAD)" == "$(git rev-parse @{u})" ]]; then
        return 1
      fi
    fi
    return 0
  )
}

amend_or_commit_version_files() {
  local reason="$1"
  echo
  echo "$reason"
  (
    cd "$REPO_ROOT"
    git status --short -- "${VERSION_FILES[@]}"
  )
  if can_amend_head; then
    if ask_yes "Amend the version-bump commit to include Cargo.lock?"; then
      (
        cd "$REPO_ROOT"
        git add -- "${VERSION_FILES[@]}"
        git commit --amend --no-edit
      )
      return 0
    fi
  fi
  if ask_yes "Create a follow-up commit for Cargo.lock only?"; then
    (
      cd "$REPO_ROOT"
      git add -- "${VERSION_FILES[@]}"
      if git diff --cached --quiet -- "${VERSION_FILES[@]}"; then
        echo "error: nothing to commit for version files" >&2
        exit 1
      fi
      git commit -m "$(cat <<EOF
Sync Cargo.lock for unifier-cli $NEW

EOF
)"
    )
    return 0
  fi
  return 1
}

if [[ "$RESUME" == true ]]; then
  if [[ -z "$OLD_HEAD" ]]; then
    echo "error: could not read unifier/Cargo.toml from HEAD" >&2
    exit 1
  fi
  NEW="$WORKING"
  OLD="$OLD_HEAD"
  if [[ "$WORKING" == "$OLD_HEAD" ]]; then
    # Version already committed; allow finishing a dirty Cargo.lock for publish.
    if [[ -z "$(
      cd "$REPO_ROOT"
      git status --porcelain -- unifier/Cargo.lock
    )" ]]; then
      echo "error: working tree version ($WORKING) matches HEAD and Cargo.lock is clean; nothing to resume" >&2
      exit 1
    fi
    echo "unifier-cli release (resume lockfix): version $NEW already on HEAD; Cargo.lock still dirty"
    FIX_LOCK_ONLY=true
  else
    echo "unifier-cli release (resume): $OLD → $NEW (using existing working-tree bump)"
    FIX_LOCK_ONLY=false
  fi
else
  FIX_LOCK_ONLY=false
  OLD="$WORKING"
  # Prefer finishing an aborted bump over stacking another bump on top of it.
  if [[ -n "$OLD_HEAD" && "$WORKING" != "$OLD_HEAD" ]]; then
    echo "Working tree already has version $WORKING (HEAD is $OLD_HEAD)."
    if ask_yes "Resume with that bump instead of bumping again?"; then
      RESUME=true
      OLD="$OLD_HEAD"
      NEW="$WORKING"
      echo "unifier-cli release (resume): $OLD → $NEW"
    else
      echo "Continuing will bump from working-tree $WORKING (not from HEAD $OLD_HEAD)."
      if ! ask_yes "Bump again from $WORKING anyway?"; then
        echo "Aborted. Use --resume to finish the existing bump, or restore files from git."
        exit 1
      fi
    fi
  elif [[ -n "$OLD_HEAD" && "$WORKING" == "$OLD_HEAD" ]]; then
    if [[ -n "$(
      cd "$REPO_ROOT"
      git status --porcelain -- unifier/Cargo.lock
    )" ]]; then
      echo "Version $WORKING is already committed, but Cargo.lock is dirty."
      if ask_yes "Resume to sync/commit Cargo.lock and publish?"; then
        RESUME=true
        FIX_LOCK_ONLY=true
        NEW="$WORKING"
        OLD="$OLD_HEAD"
        echo "unifier-cli release (resume lockfix): $NEW"
      fi
    fi
  fi

  if [[ "$RESUME" != true ]]; then
    if [[ -n "$SET_VERSION" ]]; then
      if [[ ! "$SET_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
        echo "error: --set version must be MAJOR.MINOR.PATCH (got: $SET_VERSION)" >&2
        exit 1
      fi
      NEW="$SET_VERSION"
    else
      IFS=. read -r MAJOR MINOR PATCH <<<"$OLD"
      case "$BUMP" in
        major) NEW="$((MAJOR + 1)).0.0" ;;
        minor) NEW="${MAJOR}.$((MINOR + 1)).0" ;;
        patch) NEW="${MAJOR}.${MINOR}.$((PATCH + 1))" ;;
        *) echo "error: unknown bump type $BUMP" >&2; exit 1 ;;
      esac
    fi

    if [[ "$NEW" == "$OLD" ]]; then
      echo "error: new version equals current version ($OLD)" >&2
      exit 1
    fi
    echo "unifier-cli release: $OLD → $NEW"
  fi
fi

TODAY="$(date -u +%Y-%m-%d)"
NOTES="${RELEASE_NOTES:-Release $NEW}"

# Warn if other unifier changes would be left uncommitted (version commit stays clean).
other_dirty="$(
  cd "$REPO_ROOT"
  git status --porcelain -- unifier/ \
    | awk '
        {
          path = substr($0, 4)
          if (path == "unifier/Cargo.toml" || path == "unifier/Cargo.lock" || path == "unifier/CHANGELOG.md") next
          print
        }
      '
)" || true
if [[ -n "$other_dirty" ]]; then
  echo
  echo "Note: other unifier changes exist and will NOT be included in the version commit:"
  echo "$other_dirty"
  echo
fi

sync_cargo_lock() {
  # `cargo metadata --no-deps` does NOT rewrite [[package]] version for this crate.
  # `cargo check` updates Cargo.lock to match Cargo.toml (required for cargo publish).
  echo "→ syncing Cargo.lock (cargo check)"
  # Don't let cargo consume keystrokes meant for later prompts.
  cargo check --quiet </dev/null
}

version_files_dirty() {
  (
    cd "$REPO_ROOT"
    # Non-empty if any VERSION_FILES differ from HEAD (staged or unstaged).
    git status --porcelain -- "${VERSION_FILES[@]}"
  )
}

ensure_lock_matches_toml() {
  local toml_ver lock_ver
  toml_ver="$(parse_version_file Cargo.toml)"
  lock_ver="$(
    awk '
      $0 == "name = \"unifier-cli\"" { in_pkg = 1; next }
      in_pkg && /^version = "/ {
        if (match($0, /"[0-9]+\.[0-9]+\.[0-9]+"/)) {
          print substr($0, RSTART + 1, RLENGTH - 2)
          exit
        }
      }
      in_pkg && /^\[\[/ { exit }
    ' Cargo.lock
  )"
  if [[ -z "$lock_ver" ]]; then
    echo "error: could not find unifier-cli version in Cargo.lock" >&2
    exit 1
  fi
  if [[ "$lock_ver" != "$toml_ver" ]]; then
    echo "error: Cargo.lock has unifier-cli $lock_ver but Cargo.toml has $toml_ver" >&2
    echo "hint: run cargo check, then include Cargo.lock in the version commit" >&2
    exit 1
  fi
}

if [[ "$DRY_RUN" == true ]]; then
  if [[ "${FIX_LOCK_ONLY:-false}" == true ]]; then
    echo "[dry-run] would sync Cargo.lock and amend/commit it for already-released $NEW"
  elif [[ "$RESUME" == true ]]; then
    echo "[dry-run] would reuse existing bump $OLD → $NEW"
    echo "[dry-run] would sync Cargo.lock with cargo check"
  else
    echo "[dry-run] would update Cargo.toml version"
    echo "[dry-run] would prepend CHANGELOG.md section for $NEW"
    echo "[dry-run] would sync Cargo.lock with cargo check"
  fi
  echo "[dry-run] would interactively commit only:"
  printf '  - %s\n' "${VERSION_FILES[@]}"
  if [[ "$SKIP_TESTS" != true ]]; then
    echo "[dry-run] would run: cargo test"
  fi
  echo "[dry-run] would verify Cargo.lock is clean, then: mdo publish"
  exit 0
fi

# When version is already on HEAD, do not rewrite Cargo.toml / CHANGELOG.
if [[ "$RESUME" != true && "${FIX_LOCK_ONLY:-false}" != true ]]; then
  # --- bump Cargo.toml package version (first version = line only) ---
  tmp="$(mktemp)"
  awk -v new="$NEW" '
    BEGIN { done = 0 }
    /^version = "/ && !done {
      print "version = \"" new "\""
      done = 1
      next
    }
    { print }
  ' Cargo.toml >"$tmp"
  mv "$tmp" Cargo.toml

  # --- CHANGELOG ---
  if [[ -f CHANGELOG.md ]]; then
    tmp="$(mktemp)"
    {
      head -n 1 CHANGELOG.md
      echo
      echo "## $NEW — $TODAY"
      echo
      echo "- $NOTES"
      echo
      tail -n +2 CHANGELOG.md
    } >"$tmp"
    mv "$tmp" CHANGELOG.md
  else
    cat >CHANGELOG.md <<EOF
# Changelog

## $NEW — $TODAY

- $NOTES
EOF
  fi
fi

# Always sync lock before committing (covers fresh bumps, --resume, and lock-only fix).
sync_cargo_lock
ensure_lock_matches_toml

DEFAULT_MSG="Release unifier-cli $NEW"
COMMIT_MSG="$DEFAULT_MSG"

if [[ "$FIX_LOCK_ONLY" == true ]]; then
  if [[ -z "$(version_files_dirty)" ]]; then
    echo "Cargo.lock already matches after sync; nothing to commit."
  else
    if ! amend_or_commit_version_files "Cargo.lock needs to be part of the release before publish."; then
      echo "Aborted before publish. Fix Cargo.lock, then: ./scripts/release.sh --resume"
      exit 1
    fi
  fi
else
  # --- interactive commit (version files only) ---
  echo
  echo "Version files to commit:"
  (
    cd "$REPO_ROOT"
    git status --short -- "${VERSION_FILES[@]}"
  )
  echo
  echo "Default commit message:"
  echo "  $DEFAULT_MSG"
  echo
  tty_printf "Commit message [Enter = default]: "
  tty_read COMMIT_MSG
  if [[ -z "${COMMIT_MSG// }" ]]; then
    COMMIT_MSG="$DEFAULT_MSG"
  fi

  echo
  echo "Will commit with message:"
  echo "  $COMMIT_MSG"
  echo "Files:"
  printf '  - %s\n' "${VERSION_FILES[@]}"
  echo
  if ! ask_yes "Create version-bump commit now?"; then
    echo "Aborted before commit. Version/CHANGELOG/Cargo.lock edits remain in the working tree."
    echo "Resume later with: ./scripts/release.sh --resume"
    exit 1
  fi

  (
    cd "$REPO_ROOT"
    git add -- "${VERSION_FILES[@]}"
    if git diff --cached --quiet -- "${VERSION_FILES[@]}"; then
      echo "error: nothing staged for version files; cannot create empty commit" >&2
      exit 1
    fi
    git commit -m "$(cat <<EOF
${COMMIT_MSG}

EOF
)"
  )

  echo "→ committed version bump as its own commit"
fi

ensure_lock_matches_toml

if [[ "$SKIP_TESTS" != true ]]; then
  echo "→ cargo test"
  cargo test </dev/null
fi

# cargo test / check must not leave VERSION_FILES dirty or publish will fail.
sync_cargo_lock
if [[ -n "$(version_files_dirty)" ]]; then
  if ! amend_or_commit_version_files "Version files changed after tests (usually Cargo.lock)."; then
    echo "Aborted before publish. Commit or restore Cargo.lock, then: mdo publish"
    exit 1
  fi
fi

ensure_lock_matches_toml
if [[ -n "$(version_files_dirty)" ]]; then
  echo "error: version files still dirty after sync; refusing to publish" >&2
  (
    cd "$REPO_ROOT"
    git status --short -- "${VERSION_FILES[@]}"
  )
  exit 1
fi

echo "→ publish to crates.io (mdo publish)"
if ! ask_yes "Publish unifier-cli $NEW to crates.io now?"; then
  echo "Skipped publish. Version bump is already committed."
  echo "Publish later with: mdo publish   # or: cargo publish"
  exit 0
fi

if [[ -x ../monorepo ]]; then
  ../monorepo run unifier publish
elif command -v mdo >/dev/null 2>&1; then
  mdo publish
else
  cargo publish
fi

echo "✅ Published unifier-cli $NEW to crates.io"
echo "   Tip: push the release commit when ready: git push"
