#!/usr/bin/env bash
#
# sync-upstream: move both forks onto a new upstream release.
#
# For each repo it fetches upstream, cuts a fresh v<version>-lofyer branch from
# the target tag, and replays our fork-only commits onto it. Conflicts are left
# in the worktree for you to resolve (upstream frequently reflows docs or
# deletes files we keep), then re-run with --continue.
#
# Usage:
#   scripts/sync-upstream --list              # show available upstream versions
#   scripts/sync-upstream v2.2.3              # bump both forks to v2.2.3
#   scripts/sync-upstream v2.2.3 --dry-run    # show the plan only
#   scripts/sync-upstream --continue          # resume after resolving conflicts
#
# Notes:
#   - Refuses to run with dirty worktrees (except untracked files, which are
#     yours and are left alone).
#   - The tag must exist in BOTH upstreams, so frontend and backend never end up
#     on different releases.
#   - Nothing is pushed. Review, then push each repo yourself.
#
set -o errexit
set -o nounset
set -o pipefail

source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"

TARGET=""
LIST=0
DRY_RUN=0
CONTINUE=0

while [[ $# -gt 0 ]]; do
  case "${1}" in
    --list)     LIST=1 ;;
    --dry-run)  DRY_RUN=1 ;;
    --continue) CONTINUE=1 ;;
    -h|--help)  sed -n '2,24p' "${0}"; exit 0 ;;
    v*)         TARGET="${1}" ;;
    *)          fail "unknown argument: ${1}" ;;
  esac
  shift
done

require_cmd git
require_repos

# Ensure an 'upstream' remote exists and points at GitHub.
ensure_upstream_remote() {
  local repo="${1}" url="${2}"
  if git -C "${repo}" remote get-url upstream >/dev/null 2>&1; then
    local current
    current="$(git -C "${repo}" remote get-url upstream)"
    if [[ "${current}" != "${url}" ]]; then
      log "  updating upstream remote: ${current} -> ${url}"
      git -C "${repo}" remote set-url upstream "${url}"
    fi
  else
    log "  adding upstream remote -> ${url}"
    git -C "${repo}" remote add upstream "${url}"
  fi
}

fetch_upstream() {
  local repo="${1}" url="${2}" name="${3}"
  log "fetching upstream for ${name}"
  ensure_upstream_remote "${repo}" "${url}"
  git -C "${repo}" fetch --tags --prune upstream >/dev/null 2>&1
}

# Stable release tags only (drop rc/alpha/beta), newest last.
list_versions() {
  git -C "${1}" tag --list 'v*' \
    | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
    | sort -V
}

# --- resume path -------------------------------------------------------------
if [[ "${CONTINUE}" == "1" ]]; then
  for spec in "frontend:${UI_REPO}" "backend:${BACKEND_REPO}"; do
    name="${spec%%:*}"; repo="${spec#*:}"
    if [[ -d "${repo}/.git/sequencer" ]] || [[ -f "${repo}/.git/CHERRY_PICK_HEAD" ]]; then
      step "Resuming cherry-pick in ${name}"
      if git -C "${repo}" diff --name-only --diff-filter=U | grep -q .; then
        git -C "${repo}" diff --name-only --diff-filter=U | sed 's/^/  conflict: /'
        fail "${name} still has unresolved conflicts; resolve and 'git add' them first"
      fi
      GIT_EDITOR=true git -C "${repo}" cherry-pick --continue || true
      # The repo's lint hooks can stash/restore and upset the sequencer; if the
      # commit landed, clear the leftover state so the next pick can proceed.
      if [[ -d "${repo}/.git/sequencer" ]] && ! git -C "${repo}" diff --name-only --diff-filter=U | grep -q .; then
        git -C "${repo}" cherry-pick --quit 2>/dev/null || true
      fi
      log "${name}: resumed; re-run sync-upstream with the target version to replay remaining commits"
    fi
  done
  exit 0
fi

# --- list path ---------------------------------------------------------------
fetch_upstream "${UI_REPO}" "${UI_UPSTREAM_URL}" "frontend"
fetch_upstream "${BACKEND_REPO}" "${BACKEND_UPSTREAM_URL}" "backend"

if [[ "${LIST}" == "1" ]]; then
  step "Upstream stable releases present in BOTH repos"
  comm -12 <(list_versions "${UI_REPO}") <(list_versions "${BACKEND_REPO}") | tail -15
  step "Current"
  log "frontend: $(repo_branch "${UI_REPO}")"
  log "backend : $(repo_branch "${BACKEND_REPO}")"
  exit 0
fi

[[ -n "${TARGET}" ]] || fail "no target version given (try --list)"

# --- preflight ---------------------------------------------------------------
step "Preflight"
CURRENT_VERSION="$(resolve_workspace_version)"
log "current version: ${CURRENT_VERSION}"
log "target version : ${TARGET}"
[[ "${TARGET}" != "${CURRENT_VERSION}" ]] || fail "already on ${TARGET}"

NEW_BRANCH="${TARGET}-lofyer"

for spec in "frontend:${UI_REPO}" "backend:${BACKEND_REPO}"; do
  name="${spec%%:*}"; repo="${spec#*:}"

  git -C "${repo}" rev-parse -q --verify "refs/tags/${TARGET}" >/dev/null \
    || fail "${name}: tag ${TARGET} not found upstream"

  # Tracked modifications block a clean branch cut; untracked files are the
  # user's own work and are preserved across checkout.
  if ! git -C "${repo}" diff-index --quiet HEAD -- 2>/dev/null; then
    git -C "${repo}" status --short | grep -v '^??' | sed 's/^/  /'
    fail "${name}: worktree has uncommitted changes; commit or stash them first"
  fi

  git -C "${repo}" rev-parse -q --verify "refs/heads/${NEW_BRANCH}" >/dev/null \
    && fail "${name}: branch ${NEW_BRANCH} already exists"
done
log "both repos are clean and ${TARGET} exists in both upstreams"

# Fork-only commits = on our branch but not in the new upstream tag, oldest first.
# Fork-only commits: on our branch, not reachable from the new tag, and not
# present on any upstream ref.
#
# `--cherry-pick --right-only` alone is not enough: upstream commits that were
# rebased or squashed into the new release get a different patch-id and would be
# replayed again. So we additionally drop anything that any upstream branch or
# tag contains, which is what actually distinguishes our work from upstream's.
fork_commits() {
  local repo="${1}" old_branch="${2}" c
  while read -r c; do
    [[ -n "${c}" ]] || continue
    # Skip commits reachable from any upstream remote branch or upstream tag.
    if git -C "${repo}" branch -r --contains "${c}" 2>/dev/null | grep -q '^\s*upstream/'; then
      continue
    fi
    if git -C "${repo}" tag --contains "${c}" 2>/dev/null | grep -q .; then
      continue
    fi
    echo "${c}"
  done < <(git -C "${repo}" log --reverse --no-merges --format='%H' \
             "${TARGET}..${old_branch}" --cherry-pick --right-only)
}

# --- plan --------------------------------------------------------------------
step "Plan"
declare -A PICKS
for spec in "frontend:${UI_REPO}" "backend:${BACKEND_REPO}"; do
  name="${spec%%:*}"; repo="${spec#*:}"
  old_branch="$(repo_branch "${repo}")"
  mapfile -t commits < <(fork_commits "${repo}" "${old_branch}")
  PICKS["${name}"]="${commits[*]:-}"
  log "${name}: ${old_branch} -> ${NEW_BRANCH}, replaying ${#commits[@]} fork commit(s)"
  for c in "${commits[@]:-}"; do
    [[ -n "${c}" ]] && echo "    $(git -C "${repo}" log --format='%h %s' -1 "${c}")"
  done
done

if [[ "${DRY_RUN}" == "1" ]]; then
  step "Dry run"
  log "no changes made"
  exit 0
fi

# --- execute -----------------------------------------------------------------
for spec in "frontend:${UI_REPO}" "backend:${BACKEND_REPO}"; do
  name="${spec%%:*}"; repo="${spec#*:}"
  step "Updating ${name}"

  git -C "${repo}" checkout -b "${NEW_BRANCH}" "${TARGET}"
  log "  cut ${NEW_BRANCH} from ${TARGET}"

  read -r -a commits <<< "${PICKS[${name}]}"
  for c in "${commits[@]:-}"; do
    [[ -n "${c}" ]] || continue
    subject="$(git -C "${repo}" log --format='%h %s' -1 "${c}")"
    if git -C "${repo}" cherry-pick "${c}" >/dev/null 2>&1; then
      log "  picked ${subject}"
    else
      # A lint/pre-commit hook may have committed successfully while still
      # returning non-zero; treat "no conflicts left" as success.
      if git -C "${repo}" diff --name-only --diff-filter=U | grep -q .; then
        warn "  CONFLICT while picking ${subject}"
        git -C "${repo}" diff --name-only --diff-filter=U | sed 's/^/    /'
        cat >&2 <<EOF

Resolve the conflict in ${repo}, then:
  git -C ${repo} add <files>
  $(dirname "${BASH_SOURCE[0]}")/sync-upstream --continue
  $(dirname "${BASH_SOURCE[0]}")/sync-upstream ${TARGET}   # replay the rest
EOF
        exit 1
      fi
      git -C "${repo}" cherry-pick --quit 2>/dev/null || true
      log "  picked ${subject} (hook noise ignored)"
    fi
  done
done

step "Done"
log "both forks are on ${NEW_BRANCH}"
cat <<EOF

Next steps:
  1. Verify the build:   scripts/build-image
  2. Review the history: git -C ${UI_REPO} log --oneline ${TARGET}..HEAD
                         git -C ${BACKEND_REPO} log --oneline ${TARGET}..HEAD
  3. Push both repos:    git -C ${UI_REPO} push -u origin ${NEW_BRANCH}
                         git -C ${BACKEND_REPO} push -u origin ${NEW_BRANCH}
  4. Update the version table in AGENTS.md
EOF
