#!/usr/bin/env bash
# pi-devbox-version — show which pi-devbox image build is running.
#
# WHY THIS EXISTS
#   The image bakes ground-truth build info into /etc/pi-devbox/build-manifest.json
#   at `docker build` time (see Dockerfile.variant): the release tag, build date,
#   source commit, live `pi --version` at build time, and the actual checked-out
#   commit of every /opt component clone. That answers "what image am I running?"
#   — but only if you know to go look for the file. This wraps it into one
#   command, prints it human-first at container start (see entrypoint-user.sh),
#   and stays available on demand for the rest of the session.
#
# USAGE
#   pi-devbox-version           human-readable summary (default)
#   pi-devbox-version --json    raw manifest JSON (for scripting)
#   pi-devbox-version --quiet   one-line "release_tag (source_revision)" form
#   pi-devbox-version --no-skills   skip the skill-source section (used at
#                               container start, where it would be premature)
#
# EXIT STATUS
#   0 on success. 1 if the manifest is missing (e.g. an image built before
#   this file existed, or a non-pi-devbox base) — prints a short notice
#   to stderr rather than failing silently.

set -euo pipefail

MANIFEST=/etc/pi-devbox/build-manifest.json
MODE="human"
SHOW_SKILLS="yes"

case "${1:-}" in
  --json)  MODE="json" ;;
  --quiet|-q) MODE="quiet" ;;
  --no-skills) SHOW_SKILLS="no" ;;
  --help|-h)
    sed -n '2,22p' "$0" | sed 's/^# \?//'
    exit 0
    ;;
esac

if [ ! -f "$MANIFEST" ]; then
  echo "pi-devbox-version: no build manifest at $MANIFEST" >&2
  echo "  (image predates the manifest, or this isn't a pi-devbox image)" >&2
  exit 1
fi

if ! command -v jq >/dev/null 2>&1; then
  echo "pi-devbox-version: jq not found; dumping raw manifest instead" >&2
  cat "$MANIFEST"
  exit 0
fi

if [ "$MODE" = "json" ]; then
  cat "$MANIFEST"
  exit 0
fi

release_tag=$(jq -r '.release_tag' "$MANIFEST")
build_date=$(jq -r '.build_date' "$MANIFEST")
source_rev=$(jq -r '.source_revision' "$MANIFEST")
pi_version_baked=$(jq -r '.pi_version' "$MANIFEST")
# `// empty` matters: images built before v1.8.6 have no such field, and
# `jq -r` renders a JSON null as the 4-char string "null" — which would
# print as a bogus version rather than being treated as absent.
mp_version_baked=$(jq -r '.mempalace_version // empty' "$MANIFEST")

if [ "$MODE" = "quiet" ]; then
  printf '%s (%s)\n' "$release_tag" "${source_rev:0:7}"
  exit 0
fi

# Live drift check: has `pi` been upgraded since this container was built?
# (image is immutable, but a volume-persisted ~/.pi could in theory shadow
# the baked binary — this stays honest rather than trusting the manifest
# blindly, same "ground truth over intent" spirit as how the manifest
# itself is generated in Dockerfile.variant.)
pi_version_live=""
if command -v pi >/dev/null 2>&1; then
  pi_version_live=$(pi --version 2>/dev/null | head -n1 | tr -d '\r\n')
fi

# Same check for the palace, which matters more than it looks: mempalace is
# the one component that is BOTH client (here) and server (synlig runs this
# same image), so a skew between the two is a real failure mode rather than
# cosmetic. `mempalace --version` prints "MemPalace 3.8.0" — name-prefixed,
# unlike pi's bare "0.84.3" — hence $NF rather than reading the whole line.
mp_version_live=""
if command -v mempalace >/dev/null 2>&1; then
  mp_version_live=$(mempalace --version 2>/dev/null | head -n1 | awk '{print $NF}' | tr -d '\r\n')
fi

printf 'pi-devbox %s\n' "$release_tag"
printf '  built:  %s  (source %s)\n' "$build_date" "${source_rev:0:12}"
if [ -n "$pi_version_live" ] && [ "$pi_version_live" != "$pi_version_baked" ]; then
  printf '  pi:     %s  \033[33m(baked as %s — drift detected)\033[0m\n' "$pi_version_live" "$pi_version_baked"
else
  printf '  pi:     %s\n' "${pi_version_live:-$pi_version_baked}"
fi

# Printed only when known, so this degrades quietly on pre-v1.8.6 images
# instead of showing an empty or "null" palace line.
if [ -n "$mp_version_live" ] || [ -n "$mp_version_baked" ]; then
  if [ -n "$mp_version_live" ] && [ -n "$mp_version_baked" ] && [ "$mp_version_live" != "$mp_version_baked" ]; then
    printf '  palace: %s  \033[33m(baked as %s — drift detected)\033[0m\n' "$mp_version_live" "$mp_version_baked"
  else
    printf '  palace: %s\n' "${mp_version_live:-$mp_version_baked}"
  fi
fi

printf '  components:\n'
jq -r '.components | to_entries[] | select(.value != null) | "    \(.key): \(.value[0:12])"' "$MANIFEST"

# ── Which copy of each vendored skill is actually being read? ─────────
# The image bakes fallback skills under /usr/local/share/pi-devbox/skills/,
# but for skills the skillset repo OWNS (skillset-owned.txt) a mounted live
# clone takes over at container start via devbox-skill-reconcile. Nothing
# reported which copy won, so a stale baked snapshot and a current live clone
# looked identical from inside — and on this fleet the baked mempalace copy is
# read by NOBODY (all four compose stacks mount a workspace containing the
# skillset), which is exactly the sort of fact that should be visible rather
# than reasoned about. Same "drift detected" shape as the pi/palace lines
# above: what is live, annotated with what was baked, when they disagree.
#
# Skipped with --no-skills at container start (entrypoint-user.sh calls this
# FIRST, before the baked links exist and long before the skillset deploy and
# reconcile run last), because a section that is accurate only after boot
# finishes is worse than no section at all.
BAKED_SKILLS=/usr/local/share/pi-devbox/skills
SKILLS_DIR="${HOME:-/home/developer}/.agents/skills"

if [ "$SHOW_SKILLS" = "yes" ] && [ -d "$BAKED_SKILLS" ] && [ -d "$SKILLS_DIR" ]; then
  # Recorded provenance of the vendored mempalace snapshot (absent on images
  # built before this existed — `// empty` so a JSON null never prints as the
  # 4-char string "null", the same trap noted for mempalace_version above).
  snap_ref=$(jq -r '.skillset_snapshot_ref // empty' "$MANIFEST")
  snap_sha=$(jq -r '.skillset_snapshot_sha256 // empty' "$MANIFEST")

  # Iterate the baked tree rather than a hardcoded name list, so vendoring a
  # fourth skill needs no edit here. The header prints only if the tree is
  # non-empty, so this can never emit a dangling "skills:" label.
  _printed_header="no"
  for _dir in "$BAKED_SKILLS"/*/; do
    [ -d "$_dir" ] || continue
    if [ "$_printed_header" = "no" ]; then
      printf '  skills:\n'
      _printed_header="yes"
    fi
    _name=$(basename "$_dir")
    _link="$SKILLS_DIR/$_name"

    if [ ! -e "$_link" ]; then
      printf '    %-22s not linked\n' "$_name"
      continue
    fi

    _target=$(readlink -f "$_link" 2>/dev/null || echo "$_link")
    case "$_target" in
      "$BAKED_SKILLS"/*|"$BAKED_SKILLS")
        printf '    %-22s baked\n' "$_name"
        continue
        ;;
    esac

    # Outside the baked tree: a mounted skillset clone, or a user override.
    # The link target is <repo>/skills/<name>, so the repo root is two up.
    # Everything here is guarded: this script runs on the container-start path
    # and must never fail, and `set -e` is in force.
    _root=$(cd "$_target/../.." 2>/dev/null && pwd) || _root=""
    _head=""
    if [ -n "$_root" ]; then
      _head=$(git -C "$_root" rev-parse HEAD 2>/dev/null || echo "")
    fi
    _where="live ${_root:-$_target}"
    [ -n "$_head" ] && _where="$_where @ ${_head:0:7}"

    # For the one skill whose baked fingerprint we recorded, say plainly
    # whether the live copy differs from what shipped. This is the check CI
    # cannot perform (the skillset is private) and the container can, free.
    _live_sha=""
    if [ -n "$snap_sha" ] && [ "$_name" = "mempalace" ] && [ -f "$_target/SKILL.md" ]; then
      _live_sha=$(sha256sum "$_target/SKILL.md" 2>/dev/null | cut -d' ' -f1 || echo "")
    fi
    if [ -z "$_live_sha" ]; then
      printf '    %-22s %s\n' "$_name" "$_where"
    elif [ "$_live_sha" = "$snap_sha" ]; then
      printf '    %-22s %s  (identical to baked snapshot)\n' "$_name" "$_where"
    elif [ -n "$_head" ] && [ "$_head" = "$snap_ref" ]; then
      # Same commit, different bytes — i.e. uncommitted edits in the live
      # checkout. Distinguished from plain drift because otherwise the line
      # reads as a self-contradiction ("@ c04cd15 ... baked snapshot c04cd15
      # — live copy differs") and a reader would suspect the tool, not the
      # working tree.
      printf '    %-22s %s  \033[33m(baked snapshot %s + uncommitted edits)\033[0m\n' \
        "$_name" "$_where" "${snap_ref:0:7}"
    else
      printf '    %-22s %s  \033[33m(baked snapshot %s — live copy differs)\033[0m\n' \
        "$_name" "$_where" "${snap_ref:0:7}"
    fi
  done
fi
