#!/usr/bin/env bash
# bin/release.sh — bump VERSION + tag + npm publish
# Usage:
#   ./bin/release.sh patch            # 1.3.0 -> 1.3.1
#   ./bin/release.sh minor            # 1.3.0 -> 1.4.0
#   ./bin/release.sh major            # 1.3.0 -> 2.0.0
#   ./bin/release.sh 1.5.0            # explicit version
#   ./bin/release.sh patch --dry-run  # preview only, no changes
#   DRY_RUN=1 ./bin/release.sh patch  # same via env var
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VERSION_FILE="$REPO_ROOT/VERSION"
PACKAGE_JSON="$REPO_ROOT/package.json"
CHANGELOG_MD="$REPO_ROOT/CHANGELOG.md"
SMOKE="$REPO_ROOT/bin/ayf-test"
RELEASE_DATE="$(date +%Y-%m-%d)"

# ── helpers ──────────────────────────────────────────────────────────────────

die()  { echo "ERROR: $*" >&2; exit 1; }
info() { echo "  $*"; }

require_cmd() {
  command -v "$1" >/dev/null 2>&1 || die "$1 is required but not found in PATH"
}

semver_bump() {
  local current="$1" bump="$2"
  local major minor patch
  IFS='.' read -r major minor patch <<< "$current"
  case "$bump" in
    major) major=$((major + 1)); minor=0; patch=0 ;;
    minor) minor=$((minor + 1)); patch=0 ;;
    patch) patch=$((patch + 1)) ;;
    *)     die "Unknown bump type: $bump (use major | minor | patch | <version>)" ;;
  esac
  echo "$major.$minor.$patch"
}

# Run the install smoke harness. Gate the release on it — a broken install must
# never be published. Non-destructive (ayf-test works in a temp dir).
run_smoke() {
  [[ -x "$SMOKE" || -f "$SMOKE" ]] || die "smoke harness not found at $SMOKE"
  info "Running smoke harness (bin/ayf-test)..."
  bash "$SMOKE" || die "Smoke tests failed — aborting release. Fix the install before publishing."
  info "Smoke tests passed."
}

# Promote the CHANGELOG's [Unreleased] section into a dated [NEW_VERSION] header,
# leaving a fresh empty [Unreleased] on top, and refresh the compare links.
# Keep-a-Changelog format. No-op with a warning if CHANGELOG.md is absent.
update_changelog() {
  local current="$1" next="$2" date="$3"
  if [[ ! -f "$CHANGELOG_MD" ]]; then
    echo "  ! CHANGELOG.md not found — skipping changelog update" >&2
    return 0
  fi
  info "Updating CHANGELOG.md → [$next] - $date ..."
  CL_PATH="$CHANGELOG_MD" CL_CURRENT="$current" CL_NEXT="$next" CL_DATE="$date" node -e '
    const fs = require("fs");
    const path = process.env.CL_PATH;
    const current = process.env.CL_CURRENT;
    const next = process.env.CL_NEXT;
    const date = process.env.CL_DATE;
    let s = fs.readFileSync(path, "utf8");

    // 1. Insert a dated version header right after [Unreleased], keeping
    //    [Unreleased] empty above it.
    const unrel = /^##\s*\[Unreleased\]\s*$/m;
    if (!unrel.test(s)) {
      console.error("  ! No [Unreleased] section — appending version header");
      s += `\n## [${next}] - ${date}\n`;
    } else {
      s = s.replace(unrel, `## [Unreleased]\n\n## [${next}] - ${date}\n`);
    }

    // 2. Refresh link definitions at the bottom. Derive the compare base from
    //    the existing [Unreleased] link if present.
    const baseMatch = s.match(/\[Unreleased\]:\s*(\S+?)\/compare\//);
    if (baseMatch) {
      const base = baseMatch[1];
      s = s.replace(/^\[Unreleased\]:.*$/m, `[Unreleased]: ${base}/compare/v${next}...HEAD`);
      if (!new RegExp(`^\\[${next.replace(/\./g, "\\.")}\\]:`, "m").test(s)) {
        s = s.replace(/^\[Unreleased\]:.*$/m,
          (m) => `${m}\n[${next}]: ${base}/compare/v${current}...v${next}`);
      }
    }

    fs.writeFileSync(path, s);
  ' || die "Failed to update CHANGELOG.md"
}

# ── pre-flight ────────────────────────────────────────────────────────────────

require_cmd git
require_cmd node
require_cmd npm

BUMP="${1:-}"
[[ -z "$BUMP" ]] && die "Usage: $0 <major|minor|patch|x.y.z> [--dry-run]"

# Support --dry-run flag in addition to DRY_RUN=1 env var
[[ "${2:-}" == "--dry-run" ]] && DRY_RUN=1

# Pre-flight: verify npm auth before touching anything
if [[ "${DRY_RUN:-}" != "1" ]]; then
  npm whoami >/dev/null 2>&1 || die "Not logged in to npm. Run: npm login"
fi

# Must be on main (or explicitly forced)
CURRENT_BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
if [[ "$CURRENT_BRANCH" != "main" && "${FORCE:-}" != "1" ]]; then
  die "Releases must be cut from 'main' (on '$CURRENT_BRANCH'). Set FORCE=1 to override."
fi

# Working tree must be clean
if ! git -C "$REPO_ROOT" diff --quiet HEAD; then
  die "Working tree is dirty. Commit or stash changes first."
fi

# ── resolve new version ───────────────────────────────────────────────────────

CURRENT_VERSION="$(cat "$VERSION_FILE" | tr -d '[:space:]')"
info "Current version : $CURRENT_VERSION"

if [[ "$BUMP" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  NEW_VERSION="$BUMP"
else
  NEW_VERSION="$(semver_bump "$CURRENT_VERSION" "$BUMP")"
fi

info "New version     : $NEW_VERSION"

TAG="v$NEW_VERSION"

# ── smoke gate (both real and dry-run: never release a broken install) ────────

run_smoke

# ── dry-run exit (no file changes made) ──────────────────────────────────────

if [[ "${DRY_RUN:-}" == "1" ]]; then
  echo ""
  echo "[DRY RUN] Would execute:"
  echo "  printf '$NEW_VERSION' > VERSION"
  echo "  node -e '...bump package.json to $NEW_VERSION...'"
  echo "  update CHANGELOG.md: promote [Unreleased] -> [$NEW_VERSION] - $RELEASE_DATE"
  echo "  git add VERSION package.json CHANGELOG.md && git commit -m 'chore: release v$NEW_VERSION'"
  echo "  git tag -a $TAG -m 'Release $TAG'"
  echo "  git push origin $CURRENT_BRANCH && git push origin $TAG"
  echo "  npm publish --access public"
  echo ""
  echo "[DRY RUN] Validating package contents (npm publish --dry-run)..."
  ( cd "$REPO_ROOT" && npm publish --dry-run --access public ) \
    || die "npm publish --dry-run failed — package would not publish cleanly"
  echo ""
  echo "No changes made."
  exit 0
fi

# Confirm unless CI / non-interactive
if [[ -t 0 && "${CI:-}" != "true" ]]; then
  read -r -p "  Proceed? [y/N] " CONFIRM
  [[ "$CONFIRM" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
fi

# ── bump files ────────────────────────────────────────────────────────────────

info "Bumping VERSION file..."
printf '%s\n' "$NEW_VERSION" > "$VERSION_FILE"

info "Bumping package.json..."
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('$PACKAGE_JSON', 'utf8'));
pkg.version = '$NEW_VERSION';
fs.writeFileSync('$PACKAGE_JSON', JSON.stringify(pkg, null, 2) + '\n');
"

update_changelog "$CURRENT_VERSION" "$NEW_VERSION" "$RELEASE_DATE"

# ── commit + tag ──────────────────────────────────────────────────────────────

info "Committing release..."
git -C "$REPO_ROOT" add "$VERSION_FILE" "$PACKAGE_JSON"
[[ -f "$CHANGELOG_MD" ]] && git -C "$REPO_ROOT" add "$CHANGELOG_MD"
git -C "$REPO_ROOT" commit -m "chore: release v$NEW_VERSION"

info "Tagging $TAG..."
git -C "$REPO_ROOT" tag -a "$TAG" -m "Release $TAG"

info "Pushing commit + tag to origin..."
git -C "$REPO_ROOT" push origin "$CURRENT_BRANCH"
git -C "$REPO_ROOT" push origin "$TAG"

# ── npm publish ───────────────────────────────────────────────────────────────

info "Publishing to npm..."
cd "$REPO_ROOT"
npm publish --access public

echo ""
echo "Released ay-framework@$NEW_VERSION"
echo "  git tag : $TAG"
echo "  npm     : https://www.npmjs.com/package/ay-framework"
