#!/usr/bin/env bash
# update-check.sh  -  cached version check (Phase 0 Step 0.6)
#
# Compares the locally installed pipeline version against the npm registry's
# dist-tags. Cached with a TTL so at most one network call per TTL window; the
# call is bounded by a short timeout and every failure path is silent  -  this
# script NEVER blocks and NEVER fails the pipeline (exit code is always 0).
#
# Two tags are read:
#   dist-tags.latest    newest published release  -> advisory "update available"
#   dist-tags.required  minimum version a user may run -> forced-update signal
#
# `required` is opt-in per release and set out of band, without republishing:
#   npm dist-tag add @<scope>/multi-agent-pipeline@<version> required
# Absent tag = no floor = advisory-only behaviour, exactly as before.
#
# stdout:
#   ""                        already current (or local ahead of the registry)
#   "<local>|<latest>"        a newer version exists  -  advisory
#   "<local>|<latest>|force"  local is BELOW dist-tags.required  -  the caller
#                             must halt the run until /multi-agent:update ran
# Exit code: always 0. Enforcement is the caller's job  -  see
# pipeline/scripts/require-supported-version.sh, which turns "force" into a
# non-zero exit for shell callers.
#
# Usage:
#   bash pipeline/scripts/update-check.sh                 # auto-detect local version
#   bash pipeline/scripts/update-check.sh --local 10.8.0  # explicit local version
#   bash pipeline/scripts/update-check.sh --ttl-hours 24  # cache window (default: prefs
#                                                         # updateCheck.ttlHours, else 24)
#   bash pipeline/scripts/update-check.sh --force         # ignore cache
#   bash pipeline/scripts/update-check.sh --print-required  # emit the floor only
#
# Cache file: ~/.claude/logs/multi-agent/.update-check ("epoch|latest|required").
# A legacy two-field cache ("epoch|latest") still reads  -  the missing third
# field means "no floor known", never "no floor exists".
# Registry read is a plain curl  -  never `npm view` (a user-level .npmrc scope
# mapping can silently reroute npm to a different registry; curl cannot lie).

set -euo pipefail

PKG="@mmerterden/multi-agent-pipeline"
REGISTRY_URL="https://registry.npmjs.org/${PKG/\//%2F}"
CACHE_FILE="${UPDATE_CHECK_CACHE:-$HOME/.claude/logs/multi-agent/.update-check}"
TTL_HOURS=""
FAIL_TTL_SECONDS=3600
LOCAL_VERSION=""
FORCE=0
PRINT_REQUIRED=0

while [ $# -gt 0 ]; do
  case "$1" in
    --local) LOCAL_VERSION="${2:-}"; shift; [ $# -gt 0 ] && shift ;;
    --ttl-hours) TTL_HOURS="${2:-}"; shift; [ $# -gt 0 ] && shift ;;
    --force) FORCE=1; shift ;;
    --print-required) PRINT_REQUIRED=1; shift ;;
    *) shift ;;
  esac
done

# Local version: explicit arg, else the marker the installer stamps. The
# marker is what is installed and what /multi-agent:update rewrites, so it is
# read before any repo clone; a clone is only a fallback for a machine that
# runs the pipeline from source without installing it.
if [ -z "$LOCAL_VERSION" ]; then
  for marker in "$HOME/.claude/.pipeline-version" "$HOME/.copilot/.pipeline-version" \
                "$HOME/.codex/.pipeline-version"; do
    if [ -f "$marker" ]; then
      LOCAL_VERSION=$(head -1 "$marker" 2>/dev/null | tr -d '[:space:]')
      [ -n "$LOCAL_VERSION" ] && break
    fi
  done
fi
if [ -z "$LOCAL_VERSION" ]; then
  for candidate in "$HOME/multi-agent-pipeline" "$HOME/dev/multi-agent-pipeline" "$HOME/projects/multi-agent-pipeline"; do
    if [ -f "$candidate/package.json" ]; then
      LOCAL_VERSION=$(node -p "require('$candidate/package.json').version" 2>/dev/null || true)
      [ -n "$LOCAL_VERSION" ] && break
    fi
  done
fi
[ -z "$LOCAL_VERSION" ] && exit 0  # cannot determine local version -> silent no-op

# Cache lifetime: --ttl-hours, else prefs global.updateCheck.ttlHours, else 24.
# Anything that is not a whole number falls back to 24.
if [ -z "$TTL_HOURS" ]; then
  PREFS_FILE="${MULTI_AGENT_PREFS:-$HOME/.claude/multi-agent-preferences.json}"
  if [ -f "$PREFS_FILE" ] && command -v jq >/dev/null 2>&1; then
    TTL_HOURS=$(jq -r '.global.updateCheck.ttlHours // empty' "$PREFS_FILE" 2>/dev/null || true)
  fi
fi
case "$TTL_HOURS" in (""|*[!0-9]*) TTL_HOURS=24 ;; esac

# 0 when $1 sorts strictly below $2. Semver order: numeric major.minor.patch,
# and a prerelease (1.2.3-beta) below its release (1.2.3). A leading v is ignored.
version_lt() {
  local a="${1#v}" b="${2#v}" ac ap bc bp i x y
  ac="${a%%-*}"; bc="${b%%-*}"
  ap=""; bp=""
  [ "$ac" != "$a" ] && ap="${a#*-}"
  [ "$bc" != "$b" ] && bp="${b#*-}"
  for i in 1 2 3; do
    x=$(printf '%s' "$ac" | cut -d. -f"$i"); y=$(printf '%s' "$bc" | cut -d. -f"$i")
    case "$x" in (""|*[!0-9]*) x=0 ;; esac
    case "$y" in (""|*[!0-9]*) y=0 ;; esac
    [ "$x" -lt "$y" ] && return 0
    [ "$x" -gt "$y" ] && return 1
  done
  [ -n "$ap" ] && [ -z "$bp" ] && return 0
  [ -z "$ap" ] && [ -n "$bp" ] && return 1
  [ -n "$ap" ] && [ "$(printf '%s\n%s\n' "$ap" "$bp" | sort | head -1)" = "$ap" ] && [ "$ap" != "$bp" ] && return 0
  return 1
}

now=$(date +%s)
latest=""
required=""

# Fresh cache?
if [ "$FORCE" -eq 0 ] && [ -f "$CACHE_FILE" ]; then
  cached_epoch=$(cut -d'|' -f1 "$CACHE_FILE" 2>/dev/null || echo 0)
  cached_latest=$(cut -d'|' -f2 "$CACHE_FILE" 2>/dev/null || echo "")
  cached_required=$(cut -d'|' -f3 "$CACHE_FILE" 2>/dev/null || echo "")
  case "$cached_epoch" in (*[!0-9]*|"") cached_epoch=0 ;; esac
  # A stamp in the future is a clock or copy mistake, not a fresh answer.
  [ "$cached_epoch" -gt "$now" ] && cached_epoch=0
  if [ $((now - cached_epoch)) -lt $((TTL_HOURS * 3600)) ] && [ -n "$cached_latest" ]; then
    latest="$cached_latest"
    required="$cached_required"
  fi
fi

# Stale or missing cache -> one bounded registry call (silent on any failure).
# The abbreviated packument carries dist-tags at a quarter of the full document.
# A failed read is remembered for FAIL_TTL_SECONDS in "<cache>.fail", so an
# offline machine does not wait out the timeout on every call.
if [ -z "$latest" ] && [ "$FORCE" -eq 0 ] && [ -f "$CACHE_FILE.fail" ]; then
  failed_at=$(head -1 "$CACHE_FILE.fail" 2>/dev/null | tr -d '[:space:]')
  case "$failed_at" in (*[!0-9]*|"") failed_at=0 ;; esac
  if [ "$failed_at" -le "$now" ] && [ $((now - failed_at)) -lt "$FAIL_TTL_SECONDS" ]; then
    exit 0
  fi
fi

if [ -z "$latest" ]; then
  tags=$(curl -sm 3 -H 'Accept: application/vnd.npm.install-v1+json' "$REGISTRY_URL" 2>/dev/null \
    | { if command -v jq >/dev/null 2>&1; then
          jq -r '[."dist-tags".latest // "", ."dist-tags".required // ""] | join("|")'
        else
          node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const t=JSON.parse(s)["dist-tags"]||{};process.stdout.write(`${t.latest||""}|${t.required||""}`)}catch{}})'
        fi; } ) || true
  latest="${tags%%|*}"
  required="${tags#*|}"
  [ "$required" = "$tags" ] && required=""
  mkdir -p "$(dirname "$CACHE_FILE")" 2>/dev/null || exit 0
  if [ -z "$latest" ]; then
    printf '%s\n' "$now" > "$CACHE_FILE.fail" 2>/dev/null || true
    exit 0
  fi
  rm -f "$CACHE_FILE.fail" 2>/dev/null || true
  # Written through a temp file and a rename, so a concurrent reader never
  # sees a half-written line.
  tmp_cache=$(mktemp "$CACHE_FILE.XXXXXX" 2>/dev/null) || exit 0
  if printf '%s|%s|%s\n' "$now" "$latest" "$required" > "$tmp_cache" 2>/dev/null; then
    mv -f "$tmp_cache" "$CACHE_FILE" 2>/dev/null || rm -f "$tmp_cache"
  else
    rm -f "$tmp_cache"
  fi
fi

# A floor above latest is a publisher mistake (the `required` tag was moved to a
# version that `latest` no longer covers). Clamp rather than brick every user.
if [ -n "$required" ] && version_lt "$latest" "$required"; then
  required="$latest"
fi

if [ "$PRINT_REQUIRED" -eq 1 ]; then
  printf '%s\n' "$required"
  exit 0
fi

# Forced update: local sorts strictly BELOW the required floor.
if [ -n "$required" ] && version_lt "$LOCAL_VERSION" "$required"; then
  printf '%s|%s|force\n' "$LOCAL_VERSION" "$latest"
  exit 0
fi

[ "$latest" = "$LOCAL_VERSION" ] && exit 0

# Update available only when latest sorts strictly ABOVE local (a dev machine
# running ahead of the registry must not see an "update" prompt).
if version_lt "$LOCAL_VERSION" "$latest"; then
  printf '%s|%s\n' "$LOCAL_VERSION" "$latest"
fi
exit 0
