#!/usr/bin/env bash
#
# Sync the release version (./VERSION) DOWN the channel chain so no less-stable
# branch ever trails a more-stable one.
#
#   scripts/sync-version.sh            # dry run: print what would change
#   scripts/sync-version.sh --apply    # write a VERSION-bump commit on each branch that is behind
#
# Channels flow  feature -> dev -> canary -> staging -> rc -> main  (least -> most stable;
# see BUILD.md). Each branch feeds one channel and is released independently, so a more-stable
# branch can ship a newer version than a less-stable one that hasn't been released in a while.
# That is the case this fixes: a branch's marketing version must never be *behind* a more-stable
# branch above it.
#
#   staging released 0.3.1 while dev sat at 0.2.1  ->  dev is fast-forwarded to 0.3.1
#                                                       (build number raised too, so
#                                                        CFBundleVersion stays monotonic)
#   dev already ahead at 0.3.2 vs staging 0.3.1     ->  dev is left untouched (never downgraded)
#
# Each branch is fast-forwarded to the highest marketing version among every branch listed
# before it (more stable) in the chain. Only ./VERSION is rewritten, one commit per branch:
# a non-checked-out branch is updated in place via git plumbing (no working-tree churn); the
# currently checked-out branch, if it needs a bump, is committed normally (needs a clean tree).
# Nothing is pushed — push the updated branches with your usual pushes.
#
# Branch order (most -> least stable) can be overridden with NUCLEIC_CHANNEL_BRANCHES.
# Missing branches, and branches without a readable ./VERSION, are skipped.

set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"

APPLY=0
case "${1:-}" in
  ""|-n|--dry-run) APPLY=0 ;;
  --apply)         APPLY=1 ;;
  -h|--help)
    echo "usage: $0 [--dry-run|--apply]"
    echo "  (no args / --dry-run)  show which branches would be fast-forwarded"
    echo "  --apply                write the VERSION-bump commits"
    exit 0 ;;
  *) echo "usage: $0 [--dry-run|--apply]" >&2; exit 2 ;;
esac

# Most-stable -> least-stable. Each branch is fast-forwarded up to the max marketing version
# of every branch before it in this list.
# shellcheck disable=SC2206
BRANCHES=(${NUCLEIC_CHANNEL_BRANCHES:-main rc staging canary dev})

CUR="$(git symbolic-ref --quiet --short HEAD || true)"

branch_exists() { git show-ref --verify --quiet "refs/heads/$1"; }
read_field() { git show "refs/heads/$1:VERSION" 2>/dev/null | sed -n "s/^$2=[[:space:]]*//p" | tail -n1 || true; }

# Compare two major.minor.patch strings; echo 1 (a>b), 0 (a==b) or -1 (a<b).
ver_cmp() {
  local IFS=.
  # shellcheck disable=SC2206
  local -a A=($1) B=($2)
  local i x y
  for i in 0 1 2; do
    x=${A[i]:-0}; y=${B[i]:-0}
    if ((10#$x > 10#$y)); then echo 1; return; fi
    if ((10#$x < 10#$y)); then echo -1; return; fi
  done
  echo 0
}

# Build the plan: for each branch, if its marketing version trails the running-max ("floor")
# of the more-stable branches above it, record a fast-forward. Never lowers a branch that is
# already ahead, and never touches the floor-setting (more-stable) branches themselves.
PLAN=""            # rows: branch|oldmkt|oldbuild|newmkt|newbuild|src
CUR_IN_PLAN=0
FMKT=""; FBUILD=""; FSRC=""     # floor: highest marketing version + build seen so far, and its source branch

for b in "${BRANCHES[@]}"; do
  if ! branch_exists "$b"; then
    echo "  . $b: no local branch, skipping" >&2
    continue
  fi
  mkt="$(read_field "$b" NUCLEIC_MARKETING_VERSION)"
  build="$(read_field "$b" NUCLEIC_BUILD_NUMBER)"
  if [ -z "$mkt" ] || [ -z "$build" ]; then
    echo "  . $b: no readable VERSION, skipping" >&2
    continue
  fi

  if [ -z "$FMKT" ]; then          # seed the floor from the most-stable branch that has a VERSION
    FMKT="$mkt"; FBUILD="$build"; FSRC="$b"
    continue
  fi

  if [ "$(ver_cmp "$mkt" "$FMKT")" = "-1" ]; then    # behind the floor -> fast-forward up to it
    newmkt="$FMKT"
    if [ "$build" -gt "$FBUILD" ]; then newbuild="$build"; else newbuild="$FBUILD"; fi
    PLAN="${PLAN}${b}|${mkt}|${build}|${newmkt}|${newbuild}|${FSRC}
"
    if [ "$b" = "$CUR" ]; then CUR_IN_PLAN=1; fi
  fi

  # Advance the floor to the running max (a branch that is ahead becomes the new floor for those below it).
  if [ "$(ver_cmp "$mkt" "$FMKT")" = "1" ]; then FMKT="$mkt"; FSRC="$b"; fi
  if [ "$build" -gt "$FBUILD" ]; then FBUILD="$build"; fi
done

if [ -z "$PLAN" ]; then
  echo "OK  All channel branches are in version sync — nothing to do."
  exit 0
fi

printf '%-9s %-18s    %-18s %s\n' "branch" "current" "-> new" "(catch up to)"
while IFS='|' read -r b omkt obuild nmkt nbuild src; do
  [ -n "$b" ] || continue
  printf '%-9s %-18s -> %-18s %s\n' "$b" "$omkt ($obuild)" "$nmkt ($nbuild)" "$src"
done <<EOF
$PLAN
EOF

if [ "$APPLY" != "1" ]; then
  echo
  echo "Dry run. Re-run with --apply (or 'make sync-versions APPLY=1') to write these commits."
  exit 0
fi

# Applying. If the checked-out branch itself needs a bump we commit it through the working
# tree, so require a clean tree in that case.
if [ "$CUR_IN_PLAN" = "1" ] && ! git diff-index --quiet HEAD -- 2>/dev/null; then
  echo "sync-version: working tree is dirty and the current branch ($CUR) needs a bump — commit or stash first." >&2
  exit 1
fi

echo
while IFS='|' read -r b omkt obuild nmkt nbuild src; do
  [ -n "$b" ] || continue
  msg="sync($b): VERSION $omkt ($obuild) -> $nmkt ($nbuild) — catch up to $src"
  if [ "$b" = "$CUR" ]; then
    sed -E -i.bak \
      -e "s/^NUCLEIC_MARKETING_VERSION=.*/NUCLEIC_MARKETING_VERSION=$nmkt/" \
      -e "s/^NUCLEIC_BUILD_NUMBER=.*/NUCLEIC_BUILD_NUMBER=$nbuild/" VERSION
    rm -f VERSION.bak
    git add VERSION
    git commit -q -m "$msg"
  else
    # Rewrite VERSION on the branch tip without touching the working tree or index.
    mode="$(git ls-tree "refs/heads/$b" VERSION | awk '{print $1}')"; [ -n "$mode" ] || mode=100644
    blob="$(git show "refs/heads/$b:VERSION" \
      | sed -E -e "s/^NUCLEIC_MARKETING_VERSION=.*/NUCLEIC_MARKETING_VERSION=$nmkt/" \
               -e "s/^NUCLEIC_BUILD_NUMBER=.*/NUCLEIC_BUILD_NUMBER=$nbuild/" \
      | git hash-object -w --stdin)"
    tmpidx="$(mktemp)"
    GIT_INDEX_FILE="$tmpidx" git read-tree "refs/heads/$b"
    GIT_INDEX_FILE="$tmpidx" git update-index --cacheinfo "$mode,$blob,VERSION"
    tree="$(GIT_INDEX_FILE="$tmpidx" git write-tree)"
    rm -f "$tmpidx"
    commit="$(git commit-tree "$tree" -p "refs/heads/$b" -m "$msg")"
    git update-ref "refs/heads/$b" "$commit" "refs/heads/$b"
  fi
  echo "  OK  $b -> $nmkt ($nbuild)"
done <<EOF
$PLAN
EOF

echo
echo "Version-sync commits written. Push the updated branches to publish them (e.g. git push origin dev canary)."
