#!/usr/bin/env bash
# shellcheck disable=SC1091  # Source paths resolved at runtime
# bin/josh-sync — CLI entrypoint for josh-sync
#
# Usage: josh-sync <command> [flags]
#
# Commands:
#   sync              Run forward and/or reverse sync
#   preflight         Validate config, connectivity, auth
#   import <target>   Initial import: pull subrepo into monorepo
#   reset <target>    Reset subrepo to josh-filtered view
#   onboard <target>  Import existing subrepo into monorepo (interactive)
#   migrate-pr <target> [PR#...] [--all]   Move PRs from archived to new subrepo
#   status            Show target config and sync state
#   state show|reset  Manage sync state directly
#
# Global flags:
#   --config FILE     Config path (default: .josh-sync.yml)
#   --debug           Verbose logging
#   --version         Show version
#   --help            Show usage

set -euo pipefail

# ─── Resolve library root ──────────────────────────────────────────

if [ -n "${JOSH_SYNC_ROOT:-}" ]; then
  JOSH_LIB_DIR="${JOSH_SYNC_ROOT}/lib"
else
  JOSH_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" && pwd)"
fi

# Source library modules
# shellcheck source=../lib/core.sh
source "${JOSH_LIB_DIR}/core.sh"
# shellcheck source=../lib/config.sh
source "${JOSH_LIB_DIR}/config.sh"
# shellcheck source=../lib/auth.sh
source "${JOSH_LIB_DIR}/auth.sh"
# shellcheck source=../lib/state.sh
source "${JOSH_LIB_DIR}/state.sh"
# shellcheck source=../lib/sync.sh
source "${JOSH_LIB_DIR}/sync.sh"
# shellcheck source=../lib/onboard.sh
source "${JOSH_LIB_DIR}/onboard.sh"

# ─── Version ────────────────────────────────────────────────────────

josh_sync_version() {
  local version_file
  if [ -n "${JOSH_SYNC_ROOT:-}" ]; then
    version_file="${JOSH_SYNC_ROOT}/VERSION"
  else
    version_file="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/VERSION"
  fi
  if [ -f "$version_file" ]; then
    cat "$version_file"
  else
    echo "dev"
  fi
}

# ─── Usage ──────────────────────────────────────────────────────────

usage() {
  cat >&2 <<EOF
josh-sync $(josh_sync_version) — bidirectional monorepo ↔ subrepo sync via josh-proxy

Usage: josh-sync <command> [flags]

Commands:
  sync              Run forward and/or reverse sync
  preflight         Validate config, connectivity, auth, workflow coverage
  import <target>   Initial import: pull existing subrepo into monorepo (creates PR)
  reset <target>    Reset subrepo to josh-filtered view (after merging import PR)
  onboard <target>  Import existing subrepo into monorepo (interactive, resumable)
  migrate-pr <target> [PR#...] [--all]  Move PRs from archived to new subrepo
  status            Show target config and sync state
  state show <target> [branch]    Show sync state JSON
  state reset <target> [branch]   Reset sync state to {}

Global flags:
  --config FILE     Config path (default: .josh-sync.yml)
  --debug           Verbose logging
  --version         Show version and exit
  --help            Show this help

Sync flags:
  --forward         Forward only (mono → subrepo)
  --reverse         Reverse only (subrepo → mono)
  --target NAME     Filter to target(s) — comma-separated for multiple (env: JOSH_SYNC_TARGET)
  --branch BRANCH   Filter to one branch

Environment:
  JOSH_SYNC_TARGET       Restrict to a single target name
  JOSH_SYNC_STATE_BRANCH State branch name (default: josh-sync-state)
  SYNC_BOT_USER          Git username for auth
  SYNC_BOT_TOKEN         Token with repo scope
  SUBREPO_TOKEN          Subrepo-specific token (optional)
  SUBREPO_SSH_KEY        SSH key for SSH targets (optional)
EOF
}

# ─── Sync Command ───────────────────────────────────────────────────

cmd_sync() {
  local direction="both"
  local filter_target="${JOSH_SYNC_TARGET:-}"
  local filter_branch=""
  local config_file=".josh-sync.yml"

  while [ $# -gt 0 ]; do
    case "$1" in
      --forward) direction="forward"; shift ;;
      --reverse) direction="reverse"; shift ;;
      --target)  filter_target="$2"; shift 2 ;;
      --branch)  filter_branch="$2"; shift 2 ;;
      --config)  config_file="$2"; shift 2 ;;
      --debug)   export JOSH_SYNC_DEBUG=1; shift ;;
      *) die "Unknown flag: $1" ;;
    esac
  done

  parse_config "$config_file"

  # Forward sync
  if [ "$direction" = "forward" ] || [ "$direction" = "both" ]; then
    _sync_direction "forward" "$filter_target" "$filter_branch"
  fi

  # Reverse sync
  if [ "$direction" = "reverse" ] || [ "$direction" = "both" ]; then
    _sync_direction "reverse" "$filter_target" "$filter_branch"
  fi
}

_sync_direction() {
  local direction="$1"
  local filter_target="$2"
  local filter_branch="$3"

  while read -r TARGET_JSON; do
    local target_name
    target_name=$(echo "$TARGET_JSON" | jq -r '.name')

    # Filter to specific target if requested
    if [ -n "$filter_target" ] && ! echo ",$filter_target," | grep -q ",${target_name},"; then
      continue
    fi

    log "INFO" "══════ Target: ${target_name} (${direction}) ══════"
    load_target "$TARGET_JSON"

    # Resolve branches
    local branches
    if [ -n "$filter_branch" ]; then
      branches="$filter_branch"
    elif [ "$direction" = "reverse" ]; then
      # Exclude forward_only branches for reverse sync
      branches=$(echo "$TARGET_JSON" | jq -r '
        (.forward_only // []) as $fwd |
        .branches | keys[] | select(. as $b | $fwd | index($b) | not)
      ')
    else
      branches=$(echo "$TARGET_JSON" | jq -r '.branches | keys[]')
    fi

    # Sync each branch
    for branch in $branches; do
      echo "" >&2
      log "INFO" "━━━ Processing branch: ${branch} (${direction}) ━━━"

      # Resolve branch mapping
      local mapped
      mapped=$(echo "$TARGET_JSON" | jq -r --arg b "$branch" '.branches[$b] // empty')
      if [ -z "$mapped" ]; then
        log "WARN" "No mapping for branch ${branch} — skipping"
        continue
      fi

      export SYNC_BRANCH_MONO="$branch"
      export SYNC_BRANCH_SUBREPO="$mapped"

      # Check state BEFORE cloning (skip if unchanged)
      local state prev_sha current_sha
      state=$(read_state "$branch")

      if [ "$direction" = "forward" ]; then
        prev_sha=$(echo "$state" | jq -r '.last_forward.mono_sha // empty')
        current_sha=$(git rev-parse "origin/${branch}" 2>/dev/null || echo "unknown")

        if [ "$prev_sha" = "$current_sha" ] && [ -n "$prev_sha" ]; then
          log "INFO" "Branch ${branch} unchanged since last sync — skipping"
          continue
        fi
      else
        prev_sha=$(echo "$state" | jq -r '.last_reverse.subrepo_sha // empty')
        current_sha=$(subrepo_ls_remote "${mapped}")

        if [ -z "$current_sha" ]; then
          log "WARN" "Subrepo branch ${mapped} doesn't exist — skipping"
          continue
        fi

        if [ "$prev_sha" = "$current_sha" ] && [ -n "$prev_sha" ]; then
          log "INFO" "Subrepo branch ${mapped} unchanged — skipping"
          continue
        fi
      fi

      # Check for filter change (forward only — reverse uses same filter)
      local result
      if [ "$direction" = "forward" ]; then
        local prev_filter
        prev_filter=$(echo "$state" | jq -r '.last_forward.josh_filter // empty')

        # If no filter stored (pre-v1.2 state) but a previous sync exists,
        # the old filter was the simple :/subfolder (before exclude was added)
        if [ -z "$prev_filter" ]; then
          local prev_mono_sha
          prev_mono_sha=$(echo "$state" | jq -r '.last_forward.mono_sha // empty')
          if [ -n "$prev_mono_sha" ]; then
            local subfolder
            subfolder=$(echo "$TARGET_JSON" | jq -r '.subfolder')
            prev_filter=":/${subfolder}"
          fi
        fi

        if [ -n "$prev_filter" ] && [ "$prev_filter" != "$JOSH_FILTER" ]; then
          log "WARN" "Josh filter changed — reconciling histories"
          log "INFO" "Old: ${prev_filter}"
          log "INFO" "New: ${JOSH_FILTER}"
          result=$(reconcile_filter_change)
        else
          result=$(forward_sync)
        fi
      else
        result=$(reverse_sync)
      fi
      # If forward sync hit unrelated histories, fall back to reconciliation
      if [ "$result" = "unrelated" ]; then
        log "WARN" "Unrelated histories detected — falling back to filter reconciliation"
        result=$(reconcile_filter_change)
        log "INFO" "Reconciliation result: ${result}"
      fi

      log "INFO" "Result: ${result}"

      # Handle warnings
      if [ "$result" = "lease-rejected" ]; then
        echo "::warning::Target ${target_name}, branch ${branch}: subrepo changed during sync — will retry"
        continue
      fi
      if [ "$result" = "conflict" ]; then
        echo "::warning::Target ${target_name}, branch ${branch}: merge conflict — PR created on subrepo"
        continue
      fi
      if [ "$result" = "josh-rejected" ]; then
        echo "::error::Target ${target_name}, branch ${branch}: josh rejected push — check proxy logs"
        continue
      fi

      # Update state (only on success)
      local new_state
      if [ "$direction" = "forward" ]; then
        local subrepo_sha_now
        subrepo_sha_now=$(subrepo_ls_remote "${mapped}")
        new_state=$(jq -n \
          --arg m_sha "$current_sha" \
          --arg s_sha "${subrepo_sha_now:-}" \
          --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
          --arg status "$result" \
          --arg filter "$JOSH_FILTER" \
          --argjson prev "$state" \
          '$prev + {last_forward: {mono_sha:$m_sha, subrepo_sha:$s_sha, timestamp:$ts, status:$status, josh_filter:$filter}}')
      else
        local mono_sha_now
        mono_sha_now=$(git rev-parse "origin/${branch}" 2>/dev/null || echo "")
        new_state=$(jq -n \
          --arg s_sha "$current_sha" \
          --arg m_sha "${mono_sha_now:-}" \
          --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
          --arg status "$result" \
          --argjson prev "$state" \
          '$prev + {last_reverse: {subrepo_sha:$s_sha, mono_sha:$m_sha, timestamp:$ts, status:$status}}')
      fi

      write_state "$branch" "$new_state"
    done
  done < <(echo "$JOSH_SYNC_TARGETS" | jq -c '.[]')
}

# ─── Preflight Command ─────────────────────────────────────────────

cmd_preflight() {
  local config_file=".josh-sync.yml"

  while [ $# -gt 0 ]; do
    case "$1" in
      --config) config_file="$2"; shift 2 ;;
      --debug)  export JOSH_SYNC_DEBUG=1; shift ;;
      *) die "Unknown flag: $1" ;;
    esac
  done

  local RED='\033[0;31m'
  local GREEN='\033[0;32m'
  local YELLOW='\033[1;33m'
  local NC='\033[0m'

  pass() { echo -e "  ${GREEN}✓${NC} $1"; }
  fail() { echo -e "  ${RED}✗${NC} $1"; ERRORS=$((ERRORS + 1)); }
  warn() { echo -e "  ${YELLOW}!${NC} $1"; }

  local ERRORS=0

  echo "Josh Sync — Pre-flight Check"
  echo "============================="
  echo ""

  # 1. Config
  echo "1. Configuration"

  if [ ! -f "$config_file" ]; then
    fail "${config_file} not found (run from monorepo root)"
    exit 1
  fi
  pass "${config_file} exists"

  parse_config "$config_file"

  # shellcheck disable=SC2015  # A && B || C is intentional — pass/warn/fail never fail
  {
  [ -n "$JOSH_PROXY_URL" ]  && pass "proxy_url: ${JOSH_PROXY_URL}"      || fail "proxy_url missing"
  [ -n "$MONOREPO_PATH" ]   && pass "monorepo_path: ${MONOREPO_PATH}"   || fail "monorepo_path missing"
  [ -n "$BOT_TRAILER" ]     && pass "trailer: ${BOT_TRAILER}"           || fail "trailer missing"
  [ -n "${GITEA_TOKEN:-}" ]       && pass "SYNC_BOT_TOKEN is set"       || warn "SYNC_BOT_TOKEN missing in .env"
  [ -n "${BOT_USER:-}" ]          && pass "BOT_USER: ${BOT_USER}"       || warn "BOT_USER missing in .env"
  }

  local target_count
  target_count=$(echo "$JOSH_SYNC_TARGETS" | jq 'length')
  pass "Targets configured: ${target_count}"

  # 2. Per-target checks
  echo ""
  echo "2. Targets"

  while read -r TARGET_JSON; do
    local target_name subfolder auth
    target_name=$(echo "$TARGET_JSON" | jq -r '.name')
    subfolder=$(echo "$TARGET_JSON" | jq -r '.subfolder')
    auth=$(echo "$TARGET_JSON" | jq -r '.subrepo_auth // "https"')

    echo ""
    echo "  ── Target: ${target_name} ──"

    load_target "$TARGET_JSON"

    # shellcheck disable=SC2015
    {
    [ -n "$JOSH_FILTER" ]   && pass "josh_filter: ${JOSH_FILTER}"       || fail "josh_filter missing"
    [ -n "$SUBREPO_URL" ]   && pass "subrepo_url: ${SUBREPO_URL}"       || fail "subrepo_url missing"
    }

    # Subfolder exists
    if [ -d "$subfolder" ]; then
      local file_count
      file_count=$(find "$subfolder" -type f | wc -l)
      pass "Subfolder ${subfolder}/ exists (${file_count} files)"
    else
      fail "Subfolder ${subfolder}/ not found"
      if [ -z "${CI:-}" ]; then
        echo "     If the subrepo already has content, run: josh-sync import ${target_name}"
      fi
    fi

    # SSH key check
    if [ "$auth" = "ssh" ]; then
      local local_ssh_key_var
      local_ssh_key_var=$(echo "$TARGET_JSON" | jq -r '.subrepo_ssh_key_var // "SUBREPO_SSH_KEY"')
      if [ -n "${!local_ssh_key_var:-}" ]; then
        pass "SSH key set (${local_ssh_key_var})"
      else
        warn "SSH key missing (${local_ssh_key_var})"
      fi
    fi

    # Josh proxy connectivity
    if timeout 60 git ls-remote "$(josh_auth_url)" HEAD >/dev/null 2>&1; then
      pass "Josh filter works for ${target_name}"
    else
      fail "Cannot ls-remote through josh for ${target_name}"
      echo "     Try: git ls-remote ${JOSH_PROXY_URL}/${MONOREPO_PATH}.git${JOSH_FILTER}.git"
    fi

    # Subrepo connectivity
    if [ "$auth" = "ssh" ]; then
      local local_ssh_key_var2
      local_ssh_key_var2=$(echo "$TARGET_JSON" | jq -r '.subrepo_ssh_key_var // "SUBREPO_SSH_KEY"')
      if [ -n "${!local_ssh_key_var2:-}" ]; then
        if timeout 10 git ls-remote "$(subrepo_auth_url)" HEAD >/dev/null 2>&1; then
          pass "Subrepo reachable via SSH"
        else
          fail "Cannot ls-remote subrepo via SSH"
        fi
      else
        warn "Skipping subrepo check — no SSH key"
      fi
    else
      if timeout 10 git ls-remote "$(subrepo_auth_url)" HEAD >/dev/null 2>&1; then
        pass "Subrepo reachable via HTTPS"
      else
        fail "Cannot ls-remote subrepo via HTTPS"
      fi
    fi

    # Branch mapping
    echo "$TARGET_JSON" | jq -r '
      (.forward_only // []) as $fwd |
      .branches | to_entries[] |
      .key as $k | .value as $v |
      "    mono/\($k) \(if ($fwd | index($k)) then "→ only" else "↔" end) subrepo/\($v)"
    '
  done < <(echo "$JOSH_SYNC_TARGETS" | jq -c '.[]')

  # 3. Workflow path coverage
  echo ""
  echo "3. Workflow path coverage"

  local forward_workflow=".gitea/workflows/josh-sync-forward.yml"
  if [ -f "$forward_workflow" ]; then
    pass "${forward_workflow} exists"

    local workflow_paths
    workflow_paths=$(yq -r '.on.push.paths[]?' "$forward_workflow" 2>/dev/null \
      | sed 's|/\*\*$||; s|/\*$||' || echo "")

    while read -r subfolder; do
      if echo "$workflow_paths" | grep -q "^${subfolder}$"; then
        pass "Workflow path covers ${subfolder}"
      else
        warn "Workflow paths missing ${subfolder}/** — add it to ${forward_workflow}"
      fi
    done < <(echo "$JOSH_SYNC_TARGETS" | jq -r '.[].subfolder')
  else
    warn "${forward_workflow} not found (optional for local-only use)"
  fi

  if [ -f ".gitea/workflows/josh-sync-reverse.yml" ]; then
    pass ".gitea/workflows/josh-sync-reverse.yml exists"
  else
    warn ".gitea/workflows/josh-sync-reverse.yml not found (optional)"
  fi

  # Summary
  echo ""
  echo "============================="
  if [ "$ERRORS" -eq 0 ]; then
    echo -e "${GREEN}All checks passed.${NC} Ready to deploy."
  else
    echo -e "${RED}${ERRORS} check(s) failed.${NC} Fix issues above before deploying."
    exit 1
  fi
}

# ─── Import Command ────────────────────────────────────────────────

cmd_import() {
  local config_file=".josh-sync.yml"
  local target_name=""

  while [ $# -gt 0 ]; do
    case "$1" in
      --config) config_file="$2"; shift 2 ;;
      --debug)  export JOSH_SYNC_DEBUG=1; shift ;;
      -*) die "Unknown flag: $1" ;;
      *)  target_name="$1"; shift ;;
    esac
  done

  if [ -z "$target_name" ]; then
    echo "Usage: josh-sync import <target>" >&2
    parse_config "$config_file"
    echo "Available targets:" >&2
    echo "$JOSH_SYNC_TARGETS" | jq -r '.[].name' | sed 's/^/  /' >&2
    exit 1
  fi

  parse_config "$config_file"

  local target_json
  target_json=$(echo "$JOSH_SYNC_TARGETS" | jq -c --arg n "$target_name" '.[] | select(.name == $n)')
  [ -n "$target_json" ] || die "Target '${target_name}' not found in config"

  log "INFO" "══════ Import target: ${target_name} ══════"
  load_target "$target_json"

  local branches
  branches=$(echo "$target_json" | jq -r '.branches | keys[]')

  for branch in $branches; do
    local mapped
    mapped=$(echo "$target_json" | jq -r --arg b "$branch" '.branches[$b] // empty')
    [ -z "$mapped" ] && continue

    export SYNC_BRANCH_MONO="$branch"
    export SYNC_BRANCH_SUBREPO="$mapped"

    local result
    result=$(initial_import)
    log "INFO" "Result: ${result}"
  done
}

# ─── Reset Command ─────────────────────────────────────────────────

cmd_reset() {
  local config_file=".josh-sync.yml"
  local target_name=""

  while [ $# -gt 0 ]; do
    case "$1" in
      --config) config_file="$2"; shift 2 ;;
      --debug)  export JOSH_SYNC_DEBUG=1; shift ;;
      -*) die "Unknown flag: $1" ;;
      *)  target_name="$1"; shift ;;
    esac
  done

  if [ -z "$target_name" ]; then
    echo "Usage: josh-sync reset <target>" >&2
    parse_config "$config_file"
    echo "Available targets:" >&2
    echo "$JOSH_SYNC_TARGETS" | jq -r '.[].name' | sed 's/^/  /' >&2
    exit 1
  fi

  parse_config "$config_file"

  local target_json
  target_json=$(echo "$JOSH_SYNC_TARGETS" | jq -c --arg n "$target_name" '.[] | select(.name == $n)')
  [ -n "$target_json" ] || die "Target '${target_name}' not found in config"

  log "INFO" "══════ Reset target: ${target_name} ══════"
  load_target "$target_json"

  local branches
  branches=$(echo "$target_json" | jq -r '.branches | keys[]')

  for branch in $branches; do
    local mapped
    mapped=$(echo "$target_json" | jq -r --arg b "$branch" '.branches[$b] // empty')
    [ -z "$mapped" ] && continue

    export SYNC_BRANCH_MONO="$branch"
    export SYNC_BRANCH_SUBREPO="$mapped"

    local result
    result=$(subrepo_reset)
    log "INFO" "Result: ${result}"
  done
}

# ─── Status Command ────────────────────────────────────────────────

cmd_status() {
  local config_file=".josh-sync.yml"

  while [ $# -gt 0 ]; do
    case "$1" in
      --config) config_file="$2"; shift 2 ;;
      --debug)  export JOSH_SYNC_DEBUG=1; shift ;;
      *) die "Unknown flag: $1" ;;
    esac
  done

  parse_config "$config_file"

  echo "Josh Sync Status"
  echo "================"
  echo ""
  echo "Josh proxy: ${JOSH_PROXY_URL}"
  echo "Monorepo:   ${MONOREPO_PATH}"
  echo "Bot:        ${BOT_NAME} <${BOT_EMAIL}>"
  echo ""

  while read -r TARGET_JSON; do
    local target_name subfolder auth
    target_name=$(echo "$TARGET_JSON" | jq -r '.name')
    subfolder=$(echo "$TARGET_JSON" | jq -r '.subfolder')
    auth=$(echo "$TARGET_JSON" | jq -r '.subrepo_auth // "https"')

    echo "Target: ${target_name}"
    echo "  Subfolder: ${subfolder}"
    echo "  Subrepo:   $(echo "$TARGET_JSON" | jq -r '.subrepo_url')"
    echo "  Auth:      ${auth}"
    echo "  Filter:    $(echo "$TARGET_JSON" | jq -r '.josh_filter')"

    load_target "$TARGET_JSON"

    echo "  Branches:"
    echo "$TARGET_JSON" | jq -r '.branches | to_entries[] | "    \(.key) → \(.value)"'

    local fwd_only
    fwd_only=$(echo "$TARGET_JSON" | jq -r '(.forward_only // []) | join(", ")')
    [ -n "$fwd_only" ] && echo "  Forward only: ${fwd_only}"

    # Show state for each branch
    echo "  State:"
    for branch in $(echo "$TARGET_JSON" | jq -r '.branches | keys[]'); do
      local state
      state=$(read_state "$branch")
      if [ "$state" = "{}" ]; then
        echo "    ${branch}: (no state)"
      else
        local fwd_status fwd_ts rev_status rev_ts
        fwd_status=$(echo "$state" | jq -r '.last_forward.status // "-"')
        fwd_ts=$(echo "$state" | jq -r '.last_forward.timestamp // "-"')
        rev_status=$(echo "$state" | jq -r '.last_reverse.status // "-"')
        rev_ts=$(echo "$state" | jq -r '.last_reverse.timestamp // "-"')
        echo "    ${branch}: fwd=${fwd_status} (${fwd_ts}), rev=${rev_status} (${rev_ts})"
      fi
    done
    echo ""
  done < <(echo "$JOSH_SYNC_TARGETS" | jq -c '.[]')
}

# ─── State Command ─────────────────────────────────────────────────

cmd_state() {
  local subcmd="${1:-}"
  shift || true

  local config_file=".josh-sync.yml"
  local target_name=""
  local branch="main"

  # Parse remaining args
  local args=()
  while [ $# -gt 0 ]; do
    case "$1" in
      --config) config_file="$2"; shift 2 ;;
      -*) die "Unknown flag: $1" ;;
      *)  args+=("$1"); shift ;;
    esac
  done

  [ "${#args[@]}" -ge 1 ] && target_name="${args[0]}"
  [ "${#args[@]}" -ge 2 ] && branch="${args[1]}"

  case "$subcmd" in
    show)
      [ -n "$target_name" ] || { echo "Usage: josh-sync state show <target> [branch]" >&2; exit 1; }
      parse_config "$config_file"

      local target_json
      target_json=$(echo "$JOSH_SYNC_TARGETS" | jq -c --arg n "$target_name" '.[] | select(.name == $n)')
      [ -n "$target_json" ] || die "Target '${target_name}' not found"

      load_target "$target_json"
      read_state "$branch" | jq '.'
      ;;
    reset)
      [ -n "$target_name" ] || { echo "Usage: josh-sync state reset <target> [branch]" >&2; exit 1; }
      parse_config "$config_file"

      local target_json
      target_json=$(echo "$JOSH_SYNC_TARGETS" | jq -c --arg n "$target_name" '.[] | select(.name == $n)')
      [ -n "$target_json" ] || die "Target '${target_name}' not found"

      load_target "$target_json"
      write_state "$branch" "{}"
      log "INFO" "State reset for target '${target_name}', branch '${branch}'"
      ;;
    *)
      echo "Usage: josh-sync state <show|reset> <target> [branch]" >&2
      exit 1
      ;;
  esac
}

# ─── Onboard Command ──────────────────────────────────────────────

cmd_onboard() {
  local config_file=".josh-sync.yml"
  local target_name=""
  local restart=false

  while [ $# -gt 0 ]; do
    case "$1" in
      --config)   config_file="$2"; shift 2 ;;
      --debug)    export JOSH_SYNC_DEBUG=1; shift ;;
      --restart)  restart=true; shift ;;
      -*) die "Unknown flag: $1" ;;
      *)  target_name="$1"; shift ;;
    esac
  done

  if [ -z "$target_name" ]; then
    echo "Usage: josh-sync onboard <target> [--restart]" >&2
    parse_config "$config_file"
    echo "Available targets:" >&2
    echo "$JOSH_SYNC_TARGETS" | jq -r '.[].name' | sed 's/^/  /' >&2
    exit 1
  fi

  parse_config "$config_file"

  local target_json
  target_json=$(echo "$JOSH_SYNC_TARGETS" | jq -c --arg n "$target_name" '.[] | select(.name == $n)')
  [ -n "$target_json" ] || die "Target '${target_name}' not found in config"

  log "INFO" "══════ Onboard target: ${target_name} ══════"
  load_target "$target_json"
  onboard_flow "$target_json" "$restart"
}

# ─── Migrate PR Command ──────────────────────────────────────────

cmd_migrate_pr() {
  local config_file=".josh-sync.yml"
  local target_name=""
  local all=false
  local pr_numbers=()

  while [ $# -gt 0 ]; do
    case "$1" in
      --config) config_file="$2"; shift 2 ;;
      --debug)  export JOSH_SYNC_DEBUG=1; shift ;;
      --all)    all=true; shift ;;
      -*) die "Unknown flag: $1" ;;
      *)
        if [ -z "$target_name" ]; then
          target_name="$1"
        else
          pr_numbers+=("$1")
        fi
        shift ;;
    esac
  done

  if [ -z "$target_name" ]; then
    echo "Usage: josh-sync migrate-pr <target> [PR#...] [--all]" >&2
    parse_config "$config_file"
    echo "Available targets:" >&2
    echo "$JOSH_SYNC_TARGETS" | jq -r '.[].name' | sed 's/^/  /' >&2
    exit 1
  fi

  parse_config "$config_file"

  local target_json
  target_json=$(echo "$JOSH_SYNC_TARGETS" | jq -c --arg n "$target_name" '.[] | select(.name == $n)')
  [ -n "$target_json" ] || die "Target '${target_name}' not found in config"

  load_target "$target_json"

  # Load archived repo info from onboard state
  local onboard_state archived_api
  onboard_state=$(read_onboard_state "$target_name")
  archived_api=$(echo "$onboard_state" | jq -r '.archived_api')
  if [ -z "$archived_api" ] || [ "$archived_api" = "null" ]; then
    die "No archived repo info found. Run 'josh-sync onboard ${target_name}' first."
  fi

  log "INFO" "Archived repo: ${archived_api}"

  # Load already-migrated PR numbers for skip detection and display
  local migrated_numbers
  migrated_numbers=$(echo "$onboard_state" | jq -r '[.migrated_prs // [] | .[].old_number] | map(tostring) | .[]')

  # Counters for summary
  local migrated=0 failed=0 skipped=0

  # Helper: attempt migration of one PR with counting
  _try_migrate() {
    local num="$1"
    if echo "$migrated_numbers" | grep -qx "$num"; then
      log "INFO" "PR #${num} already migrated — skipping"
      skipped=$((skipped + 1))
    elif migrate_one_pr "$num"; then
      migrated=$((migrated + 1))
    else
      failed=$((failed + 1))
    fi
  }

  if [ "$all" = true ]; then
    # Migrate all open PRs from archived repo
    local prs
    prs=$(list_open_prs "$archived_api" "$SUBREPO_TOKEN") \
      || die "Failed to list PRs on archived repo"
    local count
    count=$(echo "$prs" | jq 'length')
    log "INFO" "Found ${count} open PR(s) on archived repo"

    while read -r num; do
      _try_migrate "$num"
    done < <(echo "$prs" | jq -r '.[] | .number')

  elif [ ${#pr_numbers[@]} -gt 0 ]; then
    # Migrate specific PR numbers
    for num in "${pr_numbers[@]}"; do
      _try_migrate "$num"
    done

  else
    # Interactive: list open PRs, let user pick
    local prs
    prs=$(list_open_prs "$archived_api" "$SUBREPO_TOKEN") \
      || die "Failed to list PRs on archived repo"
    local count
    count=$(echo "$prs" | jq 'length')

    if [ "$count" -eq 0 ]; then
      log "INFO" "No open PRs on archived repo"
      return
    fi

    # Display PRs with [migrated] marker for already-processed ones
    echo "" >&2
    echo "Open PRs on archived repo:" >&2
    while IFS=$'\t' read -r num title base_ref head_ref; do
      if echo "$migrated_numbers" | grep -qx "$num"; then
        echo "  #${num}: ${title} (${base_ref} <- ${head_ref}) [migrated]" >&2
      else
        echo "  #${num}: ${title} (${base_ref} <- ${head_ref})" >&2
      fi
    done < <(echo "$prs" | jq -r '.[] | "\(.number)\t\(.title)\t\(.base.ref)\t\(.head.ref)"')
    echo "" >&2
    echo "Enter PR numbers to migrate (space-separated), or 'all':" >&2
    local selection
    read -r selection

    if [ "$selection" = "all" ]; then
      while read -r num; do
        _try_migrate "$num"
      done < <(echo "$prs" | jq -r '.[] | .number')
    else
      for num in $selection; do
        _try_migrate "$num"
      done
    fi
  fi

  log "INFO" "Migration complete: ${migrated} migrated, ${failed} failed, ${skipped} skipped"
}

# ─── Main ───────────────────────────────────────────────────────────

main() {
  local command="${1:-}"
  shift || true

  # Handle global flags that can appear before command
  case "$command" in
    --version|-v)
      echo "josh-sync $(josh_sync_version)"
      exit 0
      ;;
    --help|-h|"")
      usage
      exit 0
      ;;
  esac

  case "$command" in
    sync)       cmd_sync "$@" ;;
    preflight)  cmd_preflight "$@" ;;
    import)     cmd_import "$@" ;;
    reset)      cmd_reset "$@" ;;
    onboard)    cmd_onboard "$@" ;;
    migrate-pr) cmd_migrate_pr "$@" ;;
    status)     cmd_status "$@" ;;
    state)      cmd_state "$@" ;;
    *)
      echo "Unknown command: ${command}" >&2
      usage
      exit 1
      ;;
  esac
}

main "$@"
