#!/usr/bin/env bash

set -euo pipefail

# This script safely updates the current local branch on top of the official
# Gitea upstream without losing local work.
# It:
# 1. checks that no git operation is already in progress;
# 2. ensures the upstream remote exists and points to the official repository;
# 3. creates a local backup branch at the current HEAD;
# 4. creates a full exact snapshot of the current repository state;
# 5. stashes tracked and untracked local changes;
# 6. fetches the latest upstream changes and rebases the current branch on top
#    of upstream/main;
# 7. reapplies the local stash only after a successful rebase.
# It also offers a dry-run mode that performs the same verification flow in a
# temporary clone, so the current repository is left untouched.
# If a conflict happens, the backup branch and the stash are both kept so the
# local work can be recovered manually.

BRANCH="${BRANCH:-main}"
REMOTE_NAME="${REMOTE_NAME:-upstream}"
REMOTE_URL="${REMOTE_URL:-https://github.com/go-gitea/gitea.git}"
REPO_ROOT=""
GIT_DIR=""
STATE_FILE=""
DRY_RUN_ROOT=""
SNAPSHOT_ROOT=""
CURRENT_BRANCH=""
BACKUP_BRANCH=""
STASH_REF=""
STASH_HASH=""
STATE_BRANCH=""
STATE_BACKUP_BRANCH=""
STATE_STASH_HASH=""
STATE_REMOTE_NAME=""
STATE_REMOTE_URL=""
STATE_TARGET_BRANCH=""
STATE_START_HEAD=""
STATE_SNAPSHOT_PATH=""
STATE_STATUS=""
STATE_UPDATED_AT=""

say() {
  printf '%s\n' "$*"
}

die() {
  say "ERROR: $*" >&2
  exit 1
}

show_usage() {
  cat <<EOF
Usage: $(basename "$0") [command]

Available commands:
  sync    Run the full safe update flow: backup, stash, fetch, rebase, restore.
  dry-run Run a dry-run verification in a dedicated reusable clone without changing this repository, collecting all detected rebase conflicts.
  fetch   Fetch the latest upstream changes only.
  backup  Create a safety backup branch from the current HEAD only.
  list-backups
          List available backup branches created by this script.
  rollback [backup-branch]
          Return the current branch to a saved backup branch and restore saved local changes when possible.
  restore-stash
          Re-apply the saved pre-sync local stash from the last recorded sync state.
  clean-dry-run
          Remove the reusable dry-run workspace and legacy temporary dry-run clones.
  restore-snapshot [snapshot-dir]
          Restore the exact saved repository snapshot from the last recorded real action, or from a specific snapshot directory.
  status  Show the current repository, branch, remote, and local-change status.
  help    Show this help text.

If no command is provided and the script is run from a terminal, an interactive
menu is shown.

Recommended order:
  1. Run:  ./.update-gitea.sh status
  2. Run:  ./.update-gitea.sh dry-run
  3. If dry-run reports conflicts:
     - review the reported conflict steps, commits, and files
     - adjust or resolve the conflicting local changes in your real branch
     - run:  ./.update-gitea.sh dry-run
       again until it completes cleanly
  4. If dry-run is clean, run:  ./.update-gitea.sh sync
  5. If rebase stops with conflicts:
     - either resolve them and continue with: git rebase --continue
     - or return safely with: ./.update-gitea.sh rollback
  6. If restoring local changes after the rebase causes conflicts:
     - either resolve them manually
     - or return safely with: ./.update-gitea.sh rollback
  7. Use:  ./.update-gitea.sh list-backups
     to see the restore points created by the script.
  8. Use:  ./.update-gitea.sh restore-stash
     only when you intentionally aborted a failed sync and want the saved local
     changes reapplied again.
  9. Use:  ./.update-gitea.sh restore-snapshot
     when you want to restore the exact saved repository state, including
     untracked and ignored files that existed at snapshot time.
EOF
}

ensure_git_repo() {
  local raw_git_dir

  REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || die "This script must be run inside a git repository."
  raw_git_dir="$(git rev-parse --git-dir)"
  case "$raw_git_dir" in
    /*) GIT_DIR="$raw_git_dir" ;;
    *) GIT_DIR="$REPO_ROOT/$raw_git_dir" ;;
  esac
  STATE_FILE="$GIT_DIR/.update-gitea-state"
  DRY_RUN_ROOT="$GIT_DIR/.update-gitea-dry-run"
  SNAPSHOT_ROOT="$GIT_DIR/.update-gitea-snapshots"
}

write_state() {
  {
    printf 'STATE_BRANCH=%q\n' "$STATE_BRANCH"
    printf 'STATE_BACKUP_BRANCH=%q\n' "$STATE_BACKUP_BRANCH"
    printf 'STATE_STASH_HASH=%q\n' "$STATE_STASH_HASH"
    printf 'STATE_REMOTE_NAME=%q\n' "$STATE_REMOTE_NAME"
    printf 'STATE_REMOTE_URL=%q\n' "$STATE_REMOTE_URL"
    printf 'STATE_TARGET_BRANCH=%q\n' "$STATE_TARGET_BRANCH"
    printf 'STATE_START_HEAD=%q\n' "$STATE_START_HEAD"
    printf 'STATE_SNAPSHOT_PATH=%q\n' "$STATE_SNAPSHOT_PATH"
    printf 'STATE_STATUS=%q\n' "$STATE_STATUS"
    printf 'STATE_UPDATED_AT=%q\n' "$(date +%Y-%m-%dT%H:%M:%S)"
  } >"$STATE_FILE"
}

load_state() {
  ensure_git_repo
  [ -f "$STATE_FILE" ] || return 1
  # shellcheck disable=SC1090
  . "$STATE_FILE"
  return 0
}

clear_state() {
  ensure_git_repo
  rm -f "$STATE_FILE"
}

worktree_has_local_changes() {
  ! git diff --quiet || ! git diff --cached --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]
}

has_rebase_in_progress() {
  [ -e "$(git rev-parse --git-path rebase-merge)" ] || [ -e "$(git rev-parse --git-path rebase-apply)" ]
}

has_rebase_in_progress_in_repo() {
  local repo_path="$1"
  [ -d "$repo_path/.git/rebase-merge" ] || [ -d "$repo_path/.git/rebase-apply" ]
}

sanitize_ref_name() {
  printf '%s\n' "${1//\//-}"
}

ensure_rsync_available() {
  command -v rsync >/dev/null 2>&1 || die "rsync is required for exact repository snapshots."
}

snapshot_repo_dir() {
  printf '%s\n' "$1/repo"
}

is_valid_snapshot_dir() {
  [ -d "$1" ] && [ -d "$(snapshot_repo_dir "$1")" ]
}

create_worktree_snapshot_for_reason() {
  local reason="$1" safe_branch_name timestamp snapshot_dir snapshot_dir_repo

  ensure_rsync_available
  safe_branch_name="$(sanitize_ref_name "$CURRENT_BRANCH")"
  timestamp="$(date +%Y%m%d-%H%M%S)"
  snapshot_dir="$SNAPSHOT_ROOT/$safe_branch_name/${timestamp}-${reason}"
  snapshot_dir_repo="$(snapshot_repo_dir "$snapshot_dir")"

  mkdir -p "$snapshot_dir_repo"
  rsync --archive --delete --exclude='.git' "$REPO_ROOT"/ "$snapshot_dir_repo"/

  {
    printf 'branch=%s\n' "$CURRENT_BRANCH"
    printf 'head=%s\n' "$(git rev-parse HEAD)"
    printf 'created_at=%s\n' "$(date +%Y-%m-%dT%H:%M:%S)"
    printf 'reason=%s\n' "$reason"
  } >"$snapshot_dir/metadata.txt"

  printf '%s\n' "$snapshot_dir"
}

restore_worktree_snapshot() {
  local snapshot_dir="$1" snapshot_dir_repo

  ensure_rsync_available
  is_valid_snapshot_dir "$snapshot_dir" || die "Snapshot directory '$snapshot_dir' is not valid."
  snapshot_dir_repo="$(snapshot_repo_dir "$snapshot_dir")"

  rsync --archive --delete --exclude='.git' "$snapshot_dir_repo"/ "$REPO_ROOT"/
}

find_latest_snapshot_for_current() {
  local safe_branch_name snapshot_branch_dir
  safe_branch_name="$(sanitize_ref_name "$CURRENT_BRANCH")"
  snapshot_branch_dir="$SNAPSHOT_ROOT/$safe_branch_name"
  [ -d "$snapshot_branch_dir" ] || return 1
  find "$snapshot_branch_dir" -mindepth 1 -maxdepth 1 -type d | sort -r | head -n 1
}

ensure_no_nonrebase_git_operation_in_progress() {
  local git_path
  for git_path in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD BISECT_LOG; do
    if [ -e "$(git rev-parse --git-path "$git_path")" ]; then
      die "A git operation is already in progress ($git_path). Finish it before running this script."
    fi
  done
}

ensure_no_git_operation_in_progress() {
  local git_path
  ensure_no_nonrebase_git_operation_in_progress
  for git_path in rebase-merge rebase-apply MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD BISECT_LOG; do
    if [ -e "$(git rev-parse --git-path "$git_path")" ]; then
      die "A git operation is already in progress ($git_path). Finish it before running this script."
    fi
  done

  if [ -n "$(git diff --name-only --diff-filter=U)" ]; then
    die "There are unmerged files in the working tree. Resolve them first."
  fi
}

ensure_current_branch() {
  CURRENT_BRANCH="$(git symbolic-ref --quiet --short HEAD || true)"
  [ -n "$CURRENT_BRANCH" ] || die "Detached HEAD is not supported. Check out a branch first."
}

ensure_current_branch_with_state_fallback() {
  CURRENT_BRANCH="$(git symbolic-ref --quiet --short HEAD || true)"
  if [ -n "$CURRENT_BRANCH" ]; then
    return
  fi

  if load_state && [ -n "${STATE_BRANCH:-}" ]; then
    CURRENT_BRANCH="$STATE_BRANCH"
    return
  fi

  die "Detached HEAD is not supported here and no saved branch state was found."
}

update_state_for_sync() {
  STATE_BRANCH="$CURRENT_BRANCH"
  STATE_BACKUP_BRANCH="$BACKUP_BRANCH"
  STATE_STASH_HASH="$STASH_HASH"
  STATE_REMOTE_NAME="$REMOTE_NAME"
  STATE_REMOTE_URL="$REMOTE_URL"
  STATE_TARGET_BRANCH="$BRANCH"
  write_state
}

list_current_branch_backup_refs() {
  local safe_branch_name
  safe_branch_name="$(sanitize_ref_name "$CURRENT_BRANCH")"
  git for-each-ref --sort=-creatordate --format='%(refname:short)' "refs/heads/backup/${safe_branch_name}-*"
}

show_current_branch_backups() {
  local backup_ref

  if ! list_current_branch_backup_refs | grep -q .; then
    say "No existing backups were found for branch '$CURRENT_BRANCH'."
    return 1
  fi

  say "Existing backups for branch '$CURRENT_BRANCH':"
  while IFS= read -r backup_ref; do
    [ -n "$backup_ref" ] || continue
    say "  - $backup_ref"
  done < <(list_current_branch_backup_refs)
  return 0
}

confirm_yes_no() {
  local prompt="$1" reply

  while true; do
    printf '%s [Y/n]: ' "$prompt"
    read -r reply
    case "${reply,,}" in
      ""|y|yes) return 0 ;;
      n|no) return 1 ;;
      *) say "Please answer yes or no." ;;
    esac
  done
}

prepare_backup_before_real_action() {
  local action_label="$1" backup_reason="$2" latest_existing_backup=""

  STATE_START_HEAD="$(git rev-parse HEAD)"
  STATE_SNAPSHOT_PATH=""
  BACKUP_BRANCH=""

  latest_existing_backup="$(list_current_branch_backup_refs | head -n 1 || true)"
  if [ -z "$latest_existing_backup" ]; then
    BACKUP_BRANCH="$(create_backup_branch_for_reason "$backup_reason")"
    say "No existing backups were found, so a new backup was created automatically: $BACKUP_BRANCH"
  else
    show_current_branch_backups
    if [ -t 0 ] && [ -t 1 ]; then
      if confirm_yes_no "Create a fresh backup before '$action_label'?"; then
        BACKUP_BRANCH="$(create_backup_branch_for_reason "$backup_reason")"
        say "New backup created: $BACKUP_BRANCH"
      else
        BACKUP_BRANCH="$latest_existing_backup"
        say "No new backup created. The exact current commit was still recorded internally as: $STATE_START_HEAD"
        say "Latest existing backup selected as reference: $BACKUP_BRANCH"
      fi
    else
      BACKUP_BRANCH="$(create_backup_branch_for_reason "$backup_reason")"
      say "Non-interactive mode detected. A fresh backup was created automatically: $BACKUP_BRANCH"
    fi
  fi

  STATE_SNAPSHOT_PATH="$(create_worktree_snapshot_for_reason "$backup_reason")"
  say "Exact worktree snapshot created: $STATE_SNAPSHOT_PATH"
}

ensure_upstream_remote() {
  local current_remote_url

  if current_remote_url="$(git remote get-url "$REMOTE_NAME" 2>/dev/null)"; then
    if [ "$current_remote_url" != "$REMOTE_URL" ]; then
      die "Remote '$REMOTE_NAME' points to '$current_remote_url', expected '$REMOTE_URL'."
    fi
    return
  fi

  say "Adding remote '$REMOTE_NAME'..."
  git remote add "$REMOTE_NAME" "$REMOTE_URL"
}

show_status() {
  local stash_count latest_backup latest_snapshot dry_run_branch_dir

  ensure_git_repo
  ensure_current_branch
  ensure_upstream_remote

  stash_count="$(git stash list | wc -l | tr -d ' ')"

  say "Repository: $(git rev-parse --show-toplevel)"
  say "Current branch: $CURRENT_BRANCH"
  say "Upstream remote: $REMOTE_NAME -> $REMOTE_URL"
  say "Working tree:"
  if ! worktree_has_local_changes; then
    say "  clean"
  else
    git status --short
  fi
  say "Stash entries: $stash_count"
  latest_backup="$(find_latest_backup_branch_for_current || true)"
  say "Latest backup for current branch: ${latest_backup:-none}"
  latest_snapshot="$(find_latest_snapshot_for_current || true)"
  say "Latest snapshot for current branch: ${latest_snapshot:-none}"
  dry_run_branch_dir="$DRY_RUN_ROOT/$(sanitize_ref_name "$CURRENT_BRANCH")"
  if [ -d "$dry_run_branch_dir/repo/.git" ]; then
    say "Dry-run workspace: $dry_run_branch_dir"
  else
    say "Dry-run workspace: not initialized"
  fi
  if load_state; then
    say "Saved sync state: $STATE_STATUS"
    say "Saved backup branch: ${STATE_BACKUP_BRANCH:-none}"
    say "Saved starting commit: ${STATE_START_HEAD:-none}"
    say "Saved original stash: ${STATE_STASH_HASH:-none}"
    say "Saved exact snapshot: ${STATE_SNAPSHOT_PATH:-none}"
  else
    say "Saved sync state: none"
  fi
}

create_backup_branch_for_reason() {
  local reason="$1" safe_branch_name timestamp branch_name
  safe_branch_name="${CURRENT_BRANCH//\//-}"
  timestamp="$(date +%Y%m%d-%H%M%S)"
  branch_name="backup/${safe_branch_name}-${reason}-${timestamp}"

  git branch "$branch_name" HEAD >/dev/null
  printf '%s\n' "$branch_name"
}

create_backup_branch() {
  BACKUP_BRANCH="$(create_backup_branch_for_reason "before-upstream-sync")"
  say "Backup branch created: $BACKUP_BRANCH"
}

stash_current_changes() {
  local stash_prefix="$1" timestamp stash_name

  STASH_REF=""
  STASH_HASH=""
  if ! worktree_has_local_changes; then
    say "No local tracked/untracked changes to stash."
    return
  fi

  timestamp="$(date +%Y%m%d-%H%M%S)"
  stash_name="${stash_prefix}-${CURRENT_BRANCH//\//-}-${timestamp}"

  say "Stashing local tracked and untracked changes..."
  git stash push --include-untracked --message "$stash_name" >/dev/null
  STASH_REF="$(git stash list -1 --format='%gd')"
  [ -n "$STASH_REF" ] || die "Failed to locate the created stash entry."
  STASH_HASH="$(git rev-parse "$STASH_REF")"
  [ -n "$STASH_HASH" ] || die "Failed to resolve the created stash hash."
  say "Local changes saved in $STASH_REF"
}

stash_local_changes() {
  stash_current_changes "pre-upstream-sync"
}

fetch_upstream() {
  say "Fetching latest changes from $REMOTE_NAME/$BRANCH..."
  git fetch --prune "$REMOTE_NAME" "+refs/heads/$BRANCH:refs/remotes/$REMOTE_NAME/$BRANCH"
  git show-ref --verify --quiet "refs/remotes/$REMOTE_NAME/$BRANCH" || die "Remote branch '$REMOTE_NAME/$BRANCH' was not found."
}

show_rebase_conflict_details() {
  local repo_path="$1" conflict_label conflict_file found_conflicts=0

  conflict_label="$(get_rebase_conflict_commit_label "$repo_path")"
  if [ -n "$conflict_label" ]; then
    say "Conflicting local commit: $conflict_label"
  fi

  while IFS= read -r conflict_file; do
    if [ "$found_conflicts" -eq 0 ]; then
      say "Conflicted files:"
      found_conflicts=1
    fi
    say "  - $conflict_file"
    if [ "$conflict_file" = "package.json" ]; then
      show_package_json_conflict_details "$repo_path" "$conflict_file"
    fi
  done < <(git -C "$repo_path" diff --name-only --diff-filter=U)

  if [ "$found_conflicts" -eq 0 ]; then
    say "No explicit conflicted files were detected, but the rebase did stop."
  fi
}

get_rebase_conflict_commit_label() {
  local repo_path="$1" rebase_head

  rebase_head="$(git -C "$repo_path" rev-parse --short REBASE_HEAD 2>/dev/null || true)"
  if [ -n "$rebase_head" ]; then
    git -C "$repo_path" show --no-patch --format='%h %s' REBASE_HEAD
  fi
}

array_contains_item() {
  local item="$1" existing
  shift || true
  for existing in "$@"; do
    if [ "$existing" = "$item" ]; then
      return 0
    fi
  done
  return 1
}

extract_package_json_scripts_block() {
  sed -n '/"scripts"[[:space:]]*:[[:space:]]*{/,/^[[:space:]]*}[[:space:]]*,\{0,1\}[[:space:]]*$/p'
}

show_package_json_conflict_details() {
  local repo_path="$1" file_path="$2" upstream_side conflict_commit_side current_branch_side

  upstream_side="$(git -C "$repo_path" show ":2:$file_path" 2>/dev/null || true)"
  conflict_commit_side="$(git -C "$repo_path" show ":3:$file_path" 2>/dev/null || true)"
  current_branch_side="$(cat "$REPO_ROOT/$file_path" 2>/dev/null || true)"

  say "    package.json scripts comparison:"
  if [ -n "$upstream_side" ]; then
    say "      Upstream/current side:"
    printf '%s\n' "$upstream_side" | extract_package_json_scripts_block | sed 's/^/        /'
  fi
  if [ -n "$conflict_commit_side" ]; then
    say "      Conflicting commit side:"
    printf '%s\n' "$conflict_commit_side" | extract_package_json_scripts_block | sed 's/^/        /'
  fi
  if [ -n "$current_branch_side" ]; then
    say "      Current branch working tree side:"
    printf '%s\n' "$current_branch_side" | extract_package_json_scripts_block | sed 's/^/        /'
  fi
}

auto_resolve_current_rebase_conflicts_in_temp_clone() {
  local repo_path="$1" conflict_file

  while IFS= read -r conflict_file; do
    if ! git -C "$repo_path" checkout --theirs -- "$conflict_file" 2>/dev/null; then
      git -C "$repo_path" rm --force -- "$conflict_file" >/dev/null 2>&1 || true
    fi
    git -C "$repo_path" add -A -- "$conflict_file"
  done < <(git -C "$repo_path" diff --name-only --diff-filter=U)
}

collect_all_rebase_conflicts_in_temp_clone() {
  local repo_path="$1" conflict_count=0 commit_label conflict_file
  local -a conflict_commits=()
  local -a unique_conflict_files=()

  while has_rebase_in_progress_in_repo "$repo_path"; do
    conflict_count=$((conflict_count + 1))
    commit_label="$(get_rebase_conflict_commit_label "$repo_path")"
    if [ -n "$commit_label" ] && ! array_contains_item "$commit_label" "${conflict_commits[@]}"; then
      conflict_commits+=("$commit_label")
    fi
    while IFS= read -r conflict_file; do
      [ -n "$conflict_file" ] || continue
      if ! array_contains_item "$conflict_file" "${unique_conflict_files[@]}"; then
        unique_conflict_files+=("$conflict_file")
      fi
    done < <(git -C "$repo_path" diff --name-only --diff-filter=U)

    say "Conflict step #$conflict_count:"
    show_rebase_conflict_details "$repo_path"
    auto_resolve_current_rebase_conflicts_in_temp_clone "$repo_path"
    if ! GIT_EDITOR=: git -C "$repo_path" rebase --continue >/dev/null 2>&1; then
      if has_rebase_in_progress_in_repo "$repo_path"; then
        continue
      fi
      say "Dry-run could not continue after automatically recording a conflict step."
      return 1
    fi
  done

  say "Total detected rebase conflict steps: $conflict_count"
  if [ "${#conflict_commits[@]}" -gt 0 ]; then
    say "Local commits with conflicts (${#conflict_commits[@]}):"
    for commit_label in "${conflict_commits[@]}"; do
      say "  - $commit_label"
    done
  fi
  if [ "${#unique_conflict_files[@]}" -gt 0 ]; then
    say "Unique conflicted files (${#unique_conflict_files[@]}):"
    for conflict_file in "${unique_conflict_files[@]}"; do
      say "  - $conflict_file"
    done
  fi
  return 0
}

prepare_dry_run_workspace() {
  local dry_run_branch_dir="$1" dry_run_repo="$2"

  mkdir -p "$dry_run_branch_dir"

  if [ ! -d "$dry_run_repo/.git" ]; then
    say "Creating reusable dry-run clone in $dry_run_repo..."
    git clone --quiet --no-local --branch "$CURRENT_BRANCH" --single-branch "$REPO_ROOT" "$dry_run_repo"
  else
    say "Reusing dry-run clone in $dry_run_repo..."
    if has_rebase_in_progress_in_repo "$dry_run_repo"; then
      git -C "$dry_run_repo" rebase --abort >/dev/null 2>&1 || true
    fi
    git -C "$dry_run_repo" reset --hard >/dev/null
    git -C "$dry_run_repo" clean -fdx >/dev/null
    git -C "$dry_run_repo" remote set-url origin "$REPO_ROOT"
    git -C "$dry_run_repo" fetch --prune origin "$CURRENT_BRANCH"
    git -C "$dry_run_repo" checkout -B "$CURRENT_BRANCH" "origin/$CURRENT_BRANCH" >/dev/null 2>&1
  fi

  if git -C "$dry_run_repo" remote get-url "$REMOTE_NAME" >/dev/null 2>&1; then
    git -C "$dry_run_repo" remote set-url "$REMOTE_NAME" "$REMOTE_URL"
  else
    git -C "$dry_run_repo" remote add "$REMOTE_NAME" "$REMOTE_URL"
  fi
}

run_clean_dry_run() {
  local tmp_root legacy_dir removed_any=0

  ensure_git_repo

  if [ -d "$DRY_RUN_ROOT" ]; then
    rm -rf "$DRY_RUN_ROOT"
    say "Removed reusable dry-run workspace: $DRY_RUN_ROOT"
    removed_any=1
  fi

  tmp_root="${TMPDIR:-/tmp}"
  shopt -s nullglob
  for legacy_dir in "$tmp_root"/gitea-upstream-test.*; do
    if [ -d "$legacy_dir" ]; then
      rm -rf "$legacy_dir"
      say "Removed legacy dry-run directory: $legacy_dir"
      removed_any=1
    fi
  done
  shopt -u nullglob

  if [ "$removed_any" -eq 0 ]; then
    say "No dry-run workspace or legacy temporary directories were found."
  fi
}

rebase_onto_upstream() {
  say "Rebasing '$CURRENT_BRANCH' onto '$REMOTE_NAME/$BRANCH'..."
  if git rebase "$REMOTE_NAME/$BRANCH"; then
    say "Rebase completed successfully."
    STATE_STATUS="rebase_completed"
    update_state_for_sync
    return
  fi

  STATE_STATUS="rebase_conflict"
  update_state_for_sync
  say "Rebase stopped because of conflicts."
  show_rebase_conflict_details "."
  say "Backup branch kept at: $BACKUP_BRANCH"
  if [ -n "$STASH_HASH" ]; then
    say "Local stash kept at: $STASH_HASH"
  fi
  say "Resolve conflicts and run 'git rebase --continue', or abort with 'git rebase --abort'."
  exit 1
}

find_stash_ref_by_hash() {
  local target_hash="$1" hash ref
  while read -r hash ref; do
    if [ "$hash" = "$target_hash" ]; then
      printf '%s\n' "$ref"
      return 0
    fi
  done < <(git stash list --format='%H %gd')
  return 1
}

drop_stash_by_hash_if_present() {
  local target_hash="$1" stash_ref
  [ -n "$target_hash" ] || return 0
  if stash_ref="$(find_stash_ref_by_hash "$target_hash")"; then
    git stash drop "$stash_ref" >/dev/null
  fi
}

restore_stash() {
  local keep_saved_copy="${1:-0}" restore_target

  if [ -z "$STASH_HASH" ] && [ -z "$STASH_REF" ]; then
    return
  fi

  restore_target="${STASH_HASH:-$STASH_REF}"
  say "Restoring stashed local changes from $restore_target..."
  if git stash apply --index "$restore_target"; then
    STASH_REF=""
    if [ "$keep_saved_copy" -eq 0 ]; then
      drop_stash_by_hash_if_present "$STASH_HASH"
      STASH_HASH=""
      STATE_STASH_HASH=""
    else
      STATE_STASH_HASH="${STASH_HASH:-$restore_target}"
    fi
    STATE_STATUS="completed"
    update_state_for_sync
    if [ "$keep_saved_copy" -eq 0 ]; then
      say "Local changes restored successfully."
    else
      say "Local changes restored successfully. The original stash was kept for rollback safety."
    fi
    return
  fi

  STATE_STATUS="restore_conflict"
  update_state_for_sync
  say "Conflicts occurred while restoring local changes."
  say "Backup branch kept at: $BACKUP_BRANCH"
  say "Stash kept at: $restore_target"
  say "Resolve the conflicts manually. After that, drop the stash yourself if it is no longer needed."
  exit 1
}

find_latest_backup_branch_for_current() {
  local safe_branch_name
  safe_branch_name="$(sanitize_ref_name "$CURRENT_BRANCH")"
  git for-each-ref --sort=-creatordate --format='%(refname:short)' "refs/heads/backup/${safe_branch_name}-before-upstream-sync-*" | head -n 1
}

run_list_backups() {
  ensure_git_repo
  if ! git for-each-ref --format='%(refname:short)' refs/heads/backup/ | grep -q .; then
    say "No backup branches found."
    return
  fi

  git for-each-ref --sort=-creatordate \
    --format='%(refname:short) | %(creatordate:iso8601-local) | %(objectname:short) | %(subject)' \
    refs/heads/backup/
}

run_restore_saved_stash() {
  ensure_git_repo
  ensure_no_git_operation_in_progress
  ensure_current_branch
  load_state || die "No saved sync state was found."
  if [ -n "${STATE_BRANCH:-}" ] && [ "$CURRENT_BRANCH" != "$STATE_BRANCH" ]; then
    die "Saved sync state belongs to branch '$STATE_BRANCH'. Check out that branch before restoring the saved stash."
  fi
  [ -n "$STATE_STASH_HASH" ] || die "There is no saved pre-sync stash to restore."
  if worktree_has_local_changes; then
    die "The working tree is not clean. Commit, stash, or discard current changes before restoring the saved stash."
  fi

  prepare_backup_before_real_action "restore-stash" "before-restore-stash"

  STASH_HASH="$STATE_STASH_HASH"
  STASH_REF=""
  restore_stash 1
  say "Saved pre-sync local changes restored."
}

run_rollback() {
  local explicit_target=0 target_backup="${1:-}" rollback_safety_backup rollback_safety_snapshot=""
  local reset_target="" reset_snapshot="" preserve_stash_hash=""
  local original_restore_stash_hash="" original_restore_start_head=""
  local rollback_safety_start_head=""
  local explicit_snapshot=0

  ensure_git_repo
  ensure_no_nonrebase_git_operation_in_progress
  load_state || true
  ensure_current_branch_with_state_fallback
  ensure_upstream_remote

  if [ -n "$target_backup" ]; then
    explicit_target=1
    if is_valid_snapshot_dir "$target_backup"; then
      explicit_snapshot=1
      reset_snapshot="$target_backup"
    fi
  fi

  if [ "$explicit_target" -eq 0 ] && [ -n "${STATE_BRANCH:-}" ] && [ "$CURRENT_BRANCH" != "$STATE_BRANCH" ]; then
    die "Saved sync state belongs to branch '$STATE_BRANCH'. Check out that branch or pass an explicit backup branch."
  fi
  if [ "$explicit_snapshot" -eq 0 ] && [ "$explicit_target" -eq 0 ] && [ -n "${STATE_SNAPSHOT_PATH:-}" ] && is_valid_snapshot_dir "$STATE_SNAPSHOT_PATH"; then
    reset_snapshot="$STATE_SNAPSHOT_PATH"
  fi
  if [ "$explicit_snapshot" -eq 0 ] && [ "$explicit_target" -eq 0 ] && [ -n "${STATE_START_HEAD:-}" ]; then
    reset_target="$STATE_START_HEAD"
  fi
  if [ -z "$reset_target" ] && [ -z "$reset_snapshot" ] && [ -z "$target_backup" ]; then
    target_backup="${STATE_BACKUP_BRANCH:-}"
  fi
  if [ -z "$reset_target" ] && [ -z "$reset_snapshot" ] && [ -z "$target_backup" ]; then
    target_backup="$(find_latest_backup_branch_for_current || true)"
  fi
  if [ -z "$reset_target" ] && [ -z "$reset_snapshot" ]; then
    [ -n "$target_backup" ] || die "No backup branch was found for rollback."
    git show-ref --verify --quiet "refs/heads/$target_backup" || die "Backup branch '$target_backup' does not exist."
    reset_target="$target_backup"
  fi

  original_restore_stash_hash="${STATE_STASH_HASH:-}"
  original_restore_start_head="${STATE_START_HEAD:-}"

  if has_rebase_in_progress; then
    say "Aborting in-progress rebase before rollback..."
    git rebase --abort || die "Failed to abort the in-progress rebase."
  fi

  if [ -n "$(git diff --name-only --diff-filter=U)" ]; then
    die "There are still unmerged files in the working tree. Resolve or discard them before rollback."
  fi

  prepare_backup_before_real_action "rollback" "before-rollback"
  rollback_safety_backup="$BACKUP_BRANCH"
  rollback_safety_snapshot="$STATE_SNAPSHOT_PATH"
  rollback_safety_start_head="$STATE_START_HEAD"

  preserve_stash_hash=""
  if worktree_has_local_changes; then
    stash_current_changes "pre-rollback-preserve"
    preserve_stash_hash="$STASH_HASH"
  fi

  if [ -n "$reset_target" ]; then
    say "Resetting '$CURRENT_BRANCH' to '$reset_target'..."
    git reset --hard "$reset_target" >/dev/null
  fi

  if [ -n "$original_restore_stash_hash" ]; then
    STASH_HASH="$original_restore_stash_hash"
    STASH_REF=""
    BACKUP_BRANCH="$rollback_safety_backup"
    say "Restoring original pre-sync local changes..."
    restore_stash 1
  fi

  if [ -n "$reset_snapshot" ]; then
    say "Restoring exact worktree snapshot from: $reset_snapshot"
    restore_worktree_snapshot "$reset_snapshot"
  fi

  STATE_BRANCH="$CURRENT_BRANCH"
  STATE_BACKUP_BRANCH="$rollback_safety_backup"
  STATE_STASH_HASH="$preserve_stash_hash"
  STATE_REMOTE_NAME="$REMOTE_NAME"
  STATE_REMOTE_URL="$REMOTE_URL"
  STATE_TARGET_BRANCH="$BRANCH"
  STATE_START_HEAD="$rollback_safety_start_head"
  STATE_SNAPSHOT_PATH="$rollback_safety_snapshot"
  STATE_STATUS="rollback_completed"
  write_state

  say "Rollback completed."
  if [ -n "$reset_snapshot" ]; then
    say "Current branch and worktree restored from saved state."
    say "Restored starting commit: ${original_restore_start_head:-$reset_target}"
    say "Restored snapshot: $reset_snapshot"
  else
    say "Current branch restored from: $reset_target"
  fi
  say "Safety backup of the pre-rollback state kept at: $rollback_safety_backup"
  say "Safety snapshot of the pre-rollback state kept at: $rollback_safety_snapshot"
  if [ -n "$preserve_stash_hash" ]; then
    say "Safety stash of the pre-rollback local changes kept at: $preserve_stash_hash"
  fi
}

run_restore_snapshot() {
  local target_snapshot="${1:-}" restore_safety_backup restore_safety_snapshot=""
  local preserve_stash_hash="" restore_start_head="" restore_safety_start_head=""

  ensure_git_repo
  ensure_no_nonrebase_git_operation_in_progress
  load_state || die "No saved sync state was found."
  ensure_current_branch_with_state_fallback
  ensure_upstream_remote

  if [ -z "$target_snapshot" ]; then
    target_snapshot="${STATE_SNAPSHOT_PATH:-}"
  fi
  is_valid_snapshot_dir "$target_snapshot" || die "No valid saved snapshot was found to restore."
  restore_start_head="${STATE_START_HEAD:-}"
  [ -n "$restore_start_head" ] || die "No saved starting commit was found for the snapshot restore."

  if has_rebase_in_progress; then
    say "Aborting in-progress rebase before snapshot restore..."
    git rebase --abort || die "Failed to abort the in-progress rebase."
  fi

  if [ -n "$(git diff --name-only --diff-filter=U)" ]; then
    die "There are still unmerged files in the working tree. Resolve or discard them before restoring the snapshot."
  fi

  prepare_backup_before_real_action "restore-snapshot" "before-restore-snapshot"
  restore_safety_backup="$BACKUP_BRANCH"
  restore_safety_snapshot="$STATE_SNAPSHOT_PATH"
  restore_safety_start_head="$STATE_START_HEAD"

  if worktree_has_local_changes; then
    stash_current_changes "pre-restore-snapshot-preserve"
    preserve_stash_hash="$STASH_HASH"
  fi

  say "Resetting '$CURRENT_BRANCH' to '$restore_start_head' before snapshot restore..."
  git reset --hard "$restore_start_head" >/dev/null

  if [ -n "${STATE_STASH_HASH:-}" ]; then
    STASH_HASH="$STATE_STASH_HASH"
    STASH_REF=""
    BACKUP_BRANCH="$restore_safety_backup"
    say "Restoring saved pre-sync local changes..."
    restore_stash 1
  fi

  say "Restoring exact worktree snapshot from: $target_snapshot"
  restore_worktree_snapshot "$target_snapshot"

  STATE_BRANCH="$CURRENT_BRANCH"
  STATE_BACKUP_BRANCH="$restore_safety_backup"
  STATE_STASH_HASH="$preserve_stash_hash"
  STATE_REMOTE_NAME="$REMOTE_NAME"
  STATE_REMOTE_URL="$REMOTE_URL"
  STATE_TARGET_BRANCH="$BRANCH"
  STATE_START_HEAD="$restore_safety_start_head"
  STATE_SNAPSHOT_PATH="$restore_safety_snapshot"
  STATE_STATUS="snapshot_restored"
  write_state

  say "Snapshot restore completed."
  say "Safety backup of the pre-restore state kept at: $restore_safety_backup"
  say "Safety snapshot of the pre-restore state kept at: $restore_safety_snapshot"
  if [ -n "$preserve_stash_hash" ]; then
    say "Safety stash of the pre-restore local changes kept at: $preserve_stash_hash"
  fi
}

run_dry_run_sync() {
  local dry_run_branch_dir temp_repo patch_file has_tracked_changes has_untracked_changes
  local dry_run_has_conflicts=0 untracked_conflicts=0 path

  ensure_git_repo
  ensure_no_git_operation_in_progress
  ensure_current_branch
  ensure_upstream_remote

  dry_run_branch_dir="$DRY_RUN_ROOT/$(sanitize_ref_name "$CURRENT_BRANCH")"
  temp_repo="$dry_run_branch_dir/repo"
  patch_file="$dry_run_branch_dir/local-changes.patch"
  has_tracked_changes=0
  has_untracked_changes=0

  prepare_dry_run_workspace "$dry_run_branch_dir" "$temp_repo"

  say "Testing upstream fetch in the dry-run clone..."
  git -C "$temp_repo" fetch --prune "$REMOTE_NAME" "+refs/heads/$BRANCH:refs/remotes/$REMOTE_NAME/$BRANCH"
  git -C "$temp_repo" show-ref --verify --quiet "refs/remotes/$REMOTE_NAME/$BRANCH" || die "Remote branch '$REMOTE_NAME/$BRANCH' was not found during test."

  say "Testing rebase in the dry-run clone..."
  if ! git -C "$temp_repo" rebase "$REMOTE_NAME/$BRANCH"; then
    say "Dry-run failed during rebase."
    if ! collect_all_rebase_conflicts_in_temp_clone "$temp_repo"; then
      say "Dry-run could not enumerate all remaining rebase conflicts."
      say "Current repository was not modified."
      say "Dry-run workspace kept at: $temp_repo"
      exit 1
    fi
    dry_run_has_conflicts=1
  fi

  if ! git diff --quiet HEAD || ! git diff --cached --quiet; then
    has_tracked_changes=1
    git diff --binary HEAD >"$patch_file"
    if [ -s "$patch_file" ]; then
      say "Testing re-apply of tracked local changes..."
      if ! git -C "$temp_repo" apply --3way --index "$patch_file"; then
        say "Dry-run detected tracked local-change conflicts during re-apply."
        dry_run_has_conflicts=1
      fi
    fi
  fi

  if [ -n "$(git ls-files --others --exclude-standard)" ]; then
    has_untracked_changes=1
    say "Testing collisions for untracked local files..."
    while IFS= read -r -d '' path; do
      if [ -e "$temp_repo/$path" ]; then
        say "Untracked file would conflict after sync: $path"
        untracked_conflicts=1
      fi
    done < <(git ls-files --others --exclude-standard -z)
  fi

  if [ "$untracked_conflicts" -ne 0 ]; then
    say "Dry-run detected untracked-file conflicts."
    dry_run_has_conflicts=1
  fi

  if [ "$dry_run_has_conflicts" -ne 0 ]; then
    say "Dry-run completed with conflicts detected."
    say "A real sync would require manual conflict resolution."
    say "Current repository was not modified."
    say "Dry-run workspace kept at: $temp_repo"
    return 0
  fi

  if [ "$has_tracked_changes" -eq 0 ] && [ "$has_untracked_changes" -eq 0 ]; then
    say "Dry-run passed. No local changes needed to be re-applied."
  else
    say "Dry-run passed. Local changes appear safe to restore after sync."
  fi
  say "Dry-run workspace ready for reuse at: $temp_repo"
}

run_sync() {
  ensure_git_repo
  ensure_no_git_operation_in_progress
  ensure_current_branch
  ensure_upstream_remote
  prepare_backup_before_real_action "sync" "before-upstream-sync"
  stash_local_changes
  STATE_STATUS="prepared"
  update_state_for_sync
  fetch_upstream
  rebase_onto_upstream
  restore_stash 1
  say "Project is now synchronized with $REMOTE_NAME/$BRANCH."
  say "Safety backup branch available at: $BACKUP_BRANCH"
  say "Safety snapshot available at: $STATE_SNAPSHOT_PATH"
  if [ -n "$STATE_STASH_HASH" ]; then
    say "Original pre-sync stash retained for full rollback at: $STATE_STASH_HASH"
  fi
}

run_fetch_only() {
  ensure_git_repo
  ensure_no_git_operation_in_progress
  ensure_current_branch
  ensure_upstream_remote
  fetch_upstream
  say "Latest changes fetched from $REMOTE_NAME/$BRANCH."
}

run_backup_only() {
  ensure_git_repo
  ensure_no_git_operation_in_progress
  ensure_current_branch
  ensure_upstream_remote
  create_backup_branch
}

show_menu() {
  local choice

  while true; do
    cat <<EOF
Available actions:
  1) Full safe sync
  2) Dry-run
  3) Fetch upstream only
  4) Create backup branch only
  5) List backup branches
  6) Rollback to saved backup
  7) Restore saved stash
  8) Clean dry-run workspace
  9) Restore exact snapshot
  10) Show repository status
  11) Help
  0) Exit
EOF
    printf 'Choose an action: '
    read -r choice

    case "$choice" in
      1) run_sync; return ;;
      2) run_dry_run_sync; return ;;
      3) run_fetch_only; return ;;
      4) run_backup_only; return ;;
      5) run_list_backups ;;
      6) run_rollback; return ;;
      7) run_restore_saved_stash; return ;;
      8) run_clean_dry_run ;;
      9) run_restore_snapshot; return ;;
      10) show_status ;;
      11) show_usage ;;
      0) exit 0 ;;
      *) say "Invalid menu selection: $choice" ;;
    esac
  done
}

main() {
  case "${1:-menu}" in
    sync) run_sync ;;
    dry-run|test) run_dry_run_sync ;;
    fetch) run_fetch_only ;;
    backup) run_backup_only ;;
    list-backups) run_list_backups ;;
    rollback) run_rollback "${2:-}" ;;
    restore-stash) run_restore_saved_stash ;;
    clean-dry-run) run_clean_dry_run ;;
    restore-snapshot) run_restore_snapshot "${2:-}" ;;
    status) show_status ;;
    help|-h|--help) show_usage ;;
    menu)
      if [ -t 0 ] && [ -t 1 ]; then
        show_menu
      else
        show_usage
      fi
      ;;
    *)
      die "Unknown command: $1. Run with 'help' to see available options."
      ;;
  esac
}

main "$@"
