#!/usr/bin/env bash
# scripts/sync_to_gl.sh - copy ~/acdl contents to ~/gl/acdl and push.
#
# Copies the ACDL source tree into the GitLab mirror at ~/gl/acdl.
# Hidden files/dirs are NOT copied EXCEPT for .github (so GitLab CI
# workflows stay current) and .gitignore. The terraform/ tree is
# omitted entirely, and .gitignore patterns are honored. The
# destination's existing .git directory is preserved untouched.
#
# After syncing, commits any changes on the current branch with a
# timestamped message and pushes it to its upstream (origin/main).
#
# Run manually:
#   bash scripts/sync_to_gl.sh             # sync + commit + push
#   bash scripts/sync_to_gl.sh -v          # verbose (list copied files)
#   bash scripts/sync_to_gl.sh --no-push   # sync + commit only, no push
#   bash scripts/sync_to_gl.sh --dry-run   # show what would happen
#   SRC=~/acdl DST=~/gl/acdl bash scripts/sync_to_gl.sh
set -euo pipefail

SRC="${SRC:-$HOME/acdl}"
DST="${DST:-$HOME/gl/acdl}"
VERBOSE=0
NO_PUSH=0
DRY_RUN=0

for arg in "$@"; do
  case "$arg" in
    -v|--verbose) VERBOSE=1 ;;
    --no-push)   NO_PUSH=1 ;;
    --dry-run)   DRY_RUN=1 ;;
    -h|--help)
      sed -n '2,21p' "$0"
      exit 0
      ;;
    *) echo "FAIL: unknown argument: $arg" >&2; exit 1 ;;
  esac
done

fail() { echo "FAIL: $*" >&2; exit 1; }
run() {
  if [ "$DRY_RUN" = "1" ]; then
    echo "  [dry-run] $*"
  else
    "$@"
  fi
}

[ -d "$SRC" ] || fail "source not found: $SRC"
[ -d "$DST" ] || fail "destination not found: $DST (create it first)"
[ -d "$DST/.git" ] || fail "destination has no .git: $DST/.git (restore it first)"

echo "=== sync_to_gl ==="
echo "source:      $SRC"
echo "destination:  $DST"
[ "$NO_PUSH"    = "1" ] && echo "mode:        sync + commit (no push)"
[ "$DRY_RUN"    = "1" ] && echo "mode:        dry-run (no changes made)"
echo ""

# Sanity: refuse if DST is not inside ~/gl or is the same as SRC.
case "$DST" in
  "$HOME"/gl/*) : ;;
  *) fail "destination must live under ~/gl (got $DST)" ;;
esac
[ "$SRC" != "$DST" ] || fail "source and destination are identical"

# --- sync (rsync) -----------------------------------------------------------

# Build rsync exclude list: every hidden entry in SRC except .github
# and .gitignore.
EXCLUDES=()
for hidden in "$SRC"/.*; do
  name="$(basename "$hidden")"
  case "$name" in
    .|...) continue ;;
    .github|.gitignore) continue ;;   # keep
  esac
  EXCLUDES+=("--exclude=/$name")
done

# Never touch the destination's .git. "protect" makes rsync skip it
# entirely (neither transfer nor delete) even under --delete; this is
# stronger than --exclude, which --delete-excluded would wipe out.
# Drop it from the transfer set too.
EXCLUDES+=("--exclude=/.git")

# Omit the terraform/ tree entirely.
EXCLUDES+=("--exclude=/terraform")

# rsync filters: protect .git, then honor per-directory .gitignore
# via dir-merge (:-) semantics so patterns anchor like git does.
FILTERS=(
  "--filter=P .git"
  "--filter=:- .gitignore"
)

# Use --delete (prune extras in the synced tree) but NOT --delete-excluded:
# that would wipe destination paths covered by our --exclude rules, which
# is exactly what must NOT happen for .git.
RSYNC_ARGS=(-a --delete)
[ "$VERBOSE" = "1" ] && RSYNC_ARGS+=(-v)

echo "rsync excludes: ${EXCLUDES[*]}"
echo "rsync filters:  ${FILTERS[*]}"
echo ""

if [ "$DRY_RUN" = "1" ]; then
  echo "[dry-run] rsync would run:"
  printf '  %q ' rsync "${RSYNC_ARGS[@]}" "${FILTERS[@]}" "${EXCLUDES[@]}" "$SRC/" "$DST/"; echo
else
  rsync "${RSYNC_ARGS[@]}" "${FILTERS[@]}" "${EXCLUDES[@]}" "$SRC/" "$DST/"
  echo "rsync: OK"
fi

echo ""

# --- git commit + push ------------------------------------------------------

cd "$DST"

# Refuse to run inside a merge/rebase/conflict state.
git rev-parse --is-inside-work-tree >/dev/null
git_dir_state() {
  local f
  for f in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD BISECT_LOG; do
    [ -e ".git/$f" ] && return 1
  done
  [ -d ".git/rebase-merge" -o -d ".git/rebase-apply" ] && return 1
  return 0
}
git_dir_state || fail "destination .git is mid-operation (merge/rebase/etc); resolve it then re-run"

branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
[ -n "$branch" ] || fail "HEAD is detached; checkout a branch first (got $(git rev-parse --short HEAD))"

# Stage everything in the working tree (including deletions).
run git add -A

# Commit only if there is something staged.
if git diff --cached --quiet; then
  echo "git: no changes to commit on branch '$branch'"
else
  ts="$(date -u +%Y-%m-%d\ %H:%M\ UTC)"
  msg="chore: sync from source mirror $ts"
  echo "git: committing on branch '$branch'"
  [ "$VERBOSE" = "1" ] && git diff --cached --stat
  run git commit -m "$msg"
fi

# Push (current branch to its upstream) unless suppressed.
if [ "$NO_PUSH" = "1" ]; then
  echo "git: --no-push set, skipping push"
  PUSHED=0
else
  upstream="$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)"
  if [ -z "$upstream" ]; then
    fail "no upstream configured for branch '$branch'; set one with: git -C $DST branch --set-upstream-to=origin/$branch $branch"
  fi
  if [ "$DRY_RUN" = "1" ]; then
    echo "  [dry-run] git push to $upstream"
  else
    echo "git: pushing '$branch' to $upstream"
    git push
    echo "git: push OK"
  fi
fi

echo ""
echo "=== sync_to_gl OK ==="
echo "copied $SRC -> $DST"
[ "$DRY_RUN"    = "1" ] && echo "(dry-run: nothing actually written or pushed)"
[ "$NO_PUSH"    = "1" ] && echo "(no-push: changes committed but not pushed)"
exit 0