#!/usr/bin/env bash
set -euo pipefail

# sync-repos.sh — Clone/fetch songwhip repos into a local cache for cross-repo analysis.
#
# Usage:
#   ./sync-repos.sh '["songwhip-api","songwhip-web"]' \
#     --org theorchard \
#     --cache-dir ~/.cache/songwhip-cross-repo \
#     --max-repos 25
#
# Auth: if GITHUB_PERSONAL_ACCESS_TOKEN is set, clones/fetches are authenticated
#       with it via an ephemeral credential helper. The token is never written to
#       .git/config or passed as a process argument. Falls back to unauthenticated
#       https (public repos only) when unset.
#
# Output: JSON summary to stdout
# Exit codes: 0 = success, 1 = partial failure, 2 = fatal error

# --- Bash version check (need 4+ for associative arrays) ---
if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then
  echo '{"error": "Bash 4+ required. Install via: brew install bash"}' >&2
  exit 2
fi

# --- Defaults ---
ORG="theorchard"
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/songwhip-cross-repo"
MAX_REPOS=25

# --- Parse arguments ---
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
  cat >&2 <<'USAGE'
Usage: sync-repos.sh REPOS_JSON [OPTIONS]

Arguments:
  REPOS_JSON    JSON array of repo names, e.g. '["songwhip-api","songwhip-web"]'

Options:
  --org NAME        GitHub org (default: theorchard)
  --cache-dir PATH  Cache directory (default: ${XDG_CACHE_HOME:-$HOME/.cache}/songwhip-cross-repo)
  --max-repos N     Max repos to sync (default: 25)
  --help, -h        Show this help

Auth: set GITHUB_PERSONAL_ACCESS_TOKEN to clone private repos.
USAGE
  exit 0
fi

REPOS_JSON="${1:-}"
shift || true

while [[ $# -gt 0 ]]; do
  case "$1" in
    --org) ORG="$2"; shift 2 ;;
    --cache-dir) CACHE_DIR="$2"; shift 2 ;;
    --max-repos) MAX_REPOS="$2"; shift 2 ;;
    *) echo "Unknown argument: $1" >&2; exit 2 ;;
  esac
done

# --max-repos is later used in a numeric comparison; a non-integer would make
# `[[ -gt ]]` error and, under `set -e`, abort the script. Validate it early.
if ! [[ "$MAX_REPOS" =~ ^[0-9]+$ ]]; then
  echo "{\"error\": \"--max-repos must be a positive integer (got: $MAX_REPOS)\"}" >&2
  exit 2
fi

if [[ -z "$REPOS_JSON" ]]; then
  echo '{"error": "No repos JSON provided as first argument"}' >&2
  exit 2
fi

# --- jq is required for all JSON handling below ---
if ! command -v jq &>/dev/null; then
  echo '{"error": "jq is required but not installed. Install via: brew install jq"}' >&2
  exit 2
fi

# --- Validate JSON input ---
if ! echo "$REPOS_JSON" | jq empty 2>/dev/null; then
  echo '{"error": "Invalid JSON provided as first argument"}' >&2
  exit 2
fi

# --- Resolve cache dir (relative paths resolve against PWD) ---
if [[ "$CACHE_DIR" != /* ]]; then
  CACHE_DIR="$PWD/$CACHE_DIR"
fi

# --- Authenticated git wrapper ---
# The token is referenced (not expanded) inside the helper string, so it never
# appears in process arguments; git's credential-helper subshell reads it from
# the environment at runtime. Only clone/fetch (network ops) go through this.
git_auth() {
  if [[ -n "${GITHUB_PERSONAL_ACCESS_TOKEN:-}" ]]; then
    git -c credential.helper='!f(){ echo username=x-access-token; echo "password=$GITHUB_PERSONAL_ACCESS_TOKEN"; };f' "$@"
  else
    git "$@"
  fi
}

# --- Parse repo list ---
REPO_COUNT=$(echo "$REPOS_JSON" | jq -r 'length')
if [[ "$REPO_COUNT" -gt "$MAX_REPOS" ]]; then
  echo "{\"error\": \"Repo count ($REPO_COUNT) exceeds max ($MAX_REPOS). Use --max-repos to increase.\"}" >&2
  exit 2
fi

REPOS=()
while IFS= read -r repo; do
  [[ -n "$repo" ]] && REPOS+=("$repo")
done < <(echo "$REPOS_JSON" | jq -r '.[]')

# --- Validate repo names ---
for repo in "${REPOS[@]}"; do
  if [[ ! "$repo" =~ ^[a-zA-Z0-9._-]+$ ]]; then
    echo "{\"error\": \"Invalid repo name: $repo\"}" >&2
    exit 2
  fi
done

# --- Ensure cache directory exists ---
mkdir -p "$CACHE_DIR"

# --- Read or create manifest ---
MANIFEST="$CACHE_DIR/manifest.json"
if [[ ! -f "$MANIFEST" ]] || ! jq empty "$MANIFEST" 2>/dev/null; then
  echo '{"version": 1, "repos": {}}' > "$MANIFEST"
fi

# --- Track results ---
CLONED=0
UPDATED=0
UNCHANGED=0
FAILED=()
STALE=()
declare -A REPO_RESULTS

# --- Detect stale repos (in cache but not in discovery list) ---
CACHED_REPOS=$(jq -r '.repos | keys[]' "$MANIFEST" 2>/dev/null || echo "")
for cached in $CACHED_REPOS; do
  found=false
  for repo in "${REPOS[@]}"; do
    if [[ "$repo" == "$cached" ]]; then
      found=true
      break
    fi
  done
  if [[ "$found" == "false" ]]; then
    STALE+=("$cached")
  fi
done

# --- Sync each repo ---
for REPO_NAME in "${REPOS[@]}"; do
  REPO_DIR="$CACHE_DIR/$REPO_NAME"
  CLONE_URL="https://github.com/$ORG/$REPO_NAME.git"

  if [[ ! -d "$REPO_DIR/.git" ]]; then
    # --- Clone new repo ---
    if git_auth clone --depth=1 --single-branch "$CLONE_URL" "$REPO_DIR" 2>/dev/null; then
      NEW_SHA=$(git -C "$REPO_DIR" rev-parse HEAD)
      DEFAULT_BRANCH=$(git -C "$REPO_DIR" symbolic-ref --short HEAD 2>/dev/null || echo "master")
      REPO_RESULTS["$REPO_NAME"]="{\"status\": \"cloned\", \"sha\": \"$NEW_SHA\"}"
      CLONED=$((CLONED + 1))

      # Update manifest
      jq --arg name "$REPO_NAME" \
         --arg sha "$NEW_SHA" \
         --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
         --arg branch "$DEFAULT_BRANCH" \
         '.repos[$name] = {"lastCommitSha": $sha, "lastSynced": $ts, "defaultBranch": $branch}' \
         "$MANIFEST" > "$MANIFEST.tmp" && mv "$MANIFEST.tmp" "$MANIFEST"
    else
      FAILED+=("$REPO_NAME")
      REPO_RESULTS["$REPO_NAME"]="{\"status\": \"failed\", \"sha\": null}"
    fi
  else
    # --- Update existing repo ---
    # Safety: verify it's a shallow clone
    IS_SHALLOW=$(git -C "$REPO_DIR" rev-parse --is-shallow-repository 2>/dev/null || echo "false")
    if [[ "$IS_SHALLOW" != "true" ]]; then
      FAILED+=("$REPO_NAME")
      REPO_RESULTS["$REPO_NAME"]="{\"status\": \"failed_not_shallow\", \"sha\": null}"
      continue
    fi

    MANIFEST_SHA=$(jq -r --arg name "$REPO_NAME" '.repos[$name].lastCommitSha // ""' "$MANIFEST")

    # Fetch the default branch by name. A bare `fetch origin` populates FETCH_HEAD
    # from the configured refspec, so rev-parse/reset FETCH_HEAD can resolve to an
    # arbitrary fetched ref; naming the branch keeps it unambiguous even if the
    # clone is ever not --single-branch.
    DEFAULT_BRANCH=$(jq -r --arg name "$REPO_NAME" '.repos[$name].defaultBranch // ""' "$MANIFEST")
    [[ -z "$DEFAULT_BRANCH" ]] && DEFAULT_BRANCH=$(git -C "$REPO_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo master)

    if git_auth -C "$REPO_DIR" fetch origin "$DEFAULT_BRANCH" 2>/dev/null; then
      REMOTE_SHA=$(git -C "$REPO_DIR" rev-parse FETCH_HEAD)

      if [[ "$REMOTE_SHA" != "$MANIFEST_SHA" ]]; then
        # Safe on verified shallow read-only clones — fully updates working tree
        git -C "$REPO_DIR" reset --hard "$REMOTE_SHA" 2>/dev/null
        REPO_RESULTS["$REPO_NAME"]="{\"status\": \"updated\", \"sha\": \"$REMOTE_SHA\"}"
        UPDATED=$((UPDATED + 1))

        jq --arg name "$REPO_NAME" \
           --arg sha "$REMOTE_SHA" \
           --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
           --arg branch "$DEFAULT_BRANCH" \
           '.repos[$name] = {"lastCommitSha": $sha, "lastSynced": $ts, "defaultBranch": $branch}' \
           "$MANIFEST" > "$MANIFEST.tmp" && mv "$MANIFEST.tmp" "$MANIFEST"
      else
        REPO_RESULTS["$REPO_NAME"]="{\"status\": \"unchanged\", \"sha\": \"$MANIFEST_SHA\"}"
        UNCHANGED=$((UNCHANGED + 1))
      fi
    else
      FAILED+=("$REPO_NAME")
      REPO_RESULTS["$REPO_NAME"]="{\"status\": \"failed\", \"sha\": null}"
    fi
  fi
done

# --- Build output JSON ---
TOTAL=${#REPOS[@]}

if [[ ${#FAILED[@]} -eq 0 ]]; then
  FAILED_JSON="[]"
else
  FAILED_JSON=$(printf '%s\n' "${FAILED[@]}" | jq -R . | jq -s .)
fi

if [[ ${#STALE[@]} -eq 0 ]]; then
  STALE_JSON="[]"
else
  STALE_JSON=$(printf '%s\n' "${STALE[@]}" | jq -R . | jq -s .)
fi

# Build repos object
REPOS_OBJ="{}"
for REPO_NAME in "${REPOS[@]}"; do
  RESULT="${REPO_RESULTS[$REPO_NAME]:-"{\"status\": \"unknown\", \"sha\": null}"}"
  REPOS_OBJ=$(echo "$REPOS_OBJ" | jq --arg name "$REPO_NAME" --argjson result "$RESULT" '.[$name] = $result')
done

# Final output
jq -n \
  --argjson total "$TOTAL" \
  --argjson cloned "$CLONED" \
  --argjson updated "$UPDATED" \
  --argjson unchanged "$UNCHANGED" \
  --argjson failed "$FAILED_JSON" \
  --argjson stale "$STALE_JSON" \
  --argjson repos "$REPOS_OBJ" \
  '{
    version: 1,
    total: $total,
    cloned: $cloned,
    updated: $updated,
    unchanged: $unchanged,
    failed: $failed,
    stale: $stale,
    repos: $repos
  }'

# --- Exit code ---
if [[ ${#FAILED[@]} -gt 0 ]]; then
  if [[ $((CLONED + UPDATED + UNCHANGED)) -eq 0 ]]; then
    exit 2
  fi
  exit 1
fi
exit 0
