#!/bin/sh
# Pack the multi-platform `adapter.spt` release asset acquired from
# `BigscreenVR/omp-spt` and used by the manifest's `gh_release` update avenue.
# [impl->REQ-DIST-ADAPTER-RELEASE]
#
# Archive root:
#   manifest.toml
#   strings/{omp-spt.mjs,package.json,skills/*/SKILL.md}
#   x86_64-pc-windows-msvc/omp-spt.exe
#   x86_64-unknown-linux-gnu/omp-spt
#   x86_64-unknown-linux-musl/omp-spt
#
# spt-core classifies the recognized triple directories and flattens this
# node's binary beside the shared root. The manifest's v0.31.0 floor is above
# the v0.13.2 multi-platform archive floor. Dry-run is the default.
#
# Usage:
#   package-adapter.sh            # dry-run: validate + print the archive plan, write nothing
#   package-adapter.sh --apply    # write dist/adapter.spt (validated)
set -u
HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
ADAPTER="$ROOT/adapter"
MANIFEST="$ADAPTER/omp-spt.toml"   # renamed to manifest.toml INSIDE the archive (root-only rule)
STRINGS="$ADAPTER/strings"
EXTENSION="$STRINGS/omp-spt.mjs"
SHARED_FILES="omp-spt.mjs package.json skills/commune/SKILL.md skills/knock/SKILL.md skills/role/SKILL.md skills/setup/SKILL.md skills/signoff/SKILL.md"
TOOLS="$ROOT/tools"
OUT="${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}"   # overridable so the unit test writes to a tmp file
ARCHIVE_WRITER="$HERE/create-adapter-archive.py"
BINARY_VALIDATOR="$HERE/validate-release-binary.py"
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1

# Native Windows builds land in target/release; Linux cross-builds land in their
# target-specific release directories. Tests may override those locations.
WIN_TRIPLE=x86_64-pc-windows-msvc
GNU_TRIPLE=x86_64-unknown-linux-gnu
MUSL_TRIPLE=x86_64-unknown-linux-musl
WIN_RELSUB="${OMP_SPT_WIN_RELSUB:-release}"
GNU_RELSUB="${OMP_SPT_LINUX_RELSUB:-$GNU_TRIPLE/release}"
MUSL_RELSUB="${OMP_SPT_MUSL_RELSUB:-$MUSL_TRIPLE/release}"
BINS="omp-spt"

PY=""
for cand in "${OMP_SPT_PYTHON:-}" python python3 py; do
  [ -n "$cand" ] || continue
  command -v "$cand" >/dev/null 2>&1 || continue
  PY=$cand
  break
done
[ -n "$PY" ] || { echo "FATAL: Python is required to validate release targets and author portable archive modes"; exit 2; }

magic_of_file() {
  od -An -tx1 -N4 "$1" 2>/dev/null | tr -d '[:space:]'
}

# Validate the manifest first — refuse to ship an invalid adapter.
echo "== validate manifest =="
if ! sh "$ROOT/ci/manifest/check-manifest.sh"; then
  echo "REFUSING to package: manifest failed schema validation (fix above, then retry)." >&2
  exit 1
fi

# Every shared plugin file is load-bearing. Refuse a partially staged plugin rather than shipping
# an adapter that registers but lacks native extension discovery or a lifecycle skill.
for rel in $SHARED_FILES; do
  if [ ! -f "$STRINGS/$rel" ]; then
    echo "REFUSING to package: required shared plugin file missing: $STRINGS/$rel" >&2
    exit 1
  fi
done

# A fat archive is only readable on spt-core >= 0.13.2 — the manifest MUST declare that floor.
floor=$(grep -E '^min_spt_core_version' "$MANIFEST" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
case "$floor" in
  0.13.2|0.13.[3-9]*|0.1[4-9].*|0.[2-9][0-9].*|[1-9].*) : ;;
  *) echo "REFUSING to package: min_spt_core_version is '$floor' but a multi-platform fat .spt needs >= 0.13.2 (doyle)." >&2; exit 1 ;;
esac

# Require ALL three release binaries. The validator checks real PE32+/ELF64 x86_64 headers,
# the GNU program interpreter, and the musl target's static-link contract (no PT_INTERP or
# DT_NEEDED). Merely renaming another target's binary cannot satisfy the release.
# [impl->REQ-DIST-LINUX-MUSL]
echo
echo "== tool binaries =="
invalid=0
win_path()  { echo "$TOOLS/omp-spt/target/$WIN_RELSUB/$1.exe"; }
gnu_path()  { echo "$TOOLS/omp-spt/target/$GNU_RELSUB/$1"; }
musl_path() { echo "$TOOLS/omp-spt/target/$MUSL_RELSUB/$1"; }
validate_binary() {
  target=$1
  path=$2
  if [ ! -f "$path" ]; then
    echo "  MISS  $path"
    invalid=1
  elif ! "$PY" "$BINARY_VALIDATOR" "$target" "$path"; then
    invalid=1
  fi
}
for b in $BINS; do
  validate_binary "$WIN_TRIPLE" "$(win_path "$b")"
  validate_binary "$GNU_TRIPLE" "$(gnu_path "$b")"
  validate_binary "$MUSL_TRIPLE" "$(musl_path "$b")"
done
if [ "$invalid" -ne 0 ]; then
  echo "REFUSING to package: build valid binaries for ALL release targets — Windows: sh ci/digest/build.sh;" >&2
  echo "GNU + static musl Linux: cargo zigbuild --release --target <triple> --manifest-path tools/omp-spt/Cargo.toml" >&2
  echo "(see docs/RELEASE-RUNBOOK.md)." >&2
  exit 1
fi

echo
echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="
echo "manifest : $MANIFEST  ->  (root) manifest.toml          [min_spt_core $floor]"
echo "plugin   : $STRINGS  ->  (root) strings/{omp-spt.mjs,package.json,skills/*/SKILL.md}"
echo "win bin  : -> $WIN_TRIPLE/omp-spt.exe"
echo "GNU bin  : -> $GNU_TRIPLE/omp-spt"
echo "musl bin : -> $MUSL_TRIPLE/omp-spt  [ELF64 x86_64, static]"
echo "asset    : $OUT  (the single fat adapter.spt — auto-resolves the host triple on install)"

if [ "$APPLY" -ne 1 ]; then
  echo
  echo "DRY-RUN: nothing written. Re-run with --apply to write $OUT, then attach it as 'adapter.spt'"
  echo "to a GitHub release on the monorepo. End users: 'spt adapter add --release BigscreenVR/omp-spt'"
  echo "(default asset adapter.spt). Needs spt v$floor+. See docs/RELEASE-RUNBOOK.md."
  exit 0
fi

# Stage the exact archive root, then write it with explicit portable member modes. Python's tar
# writer authors both Linux files as 0755 even when cross-packaging on MSYS/NTFS.
STAGE=$(mktemp -d "${TMPDIR:-/tmp}/omp-spt-adapter.XXXXXX") || { echo "FATAL: mktemp failed"; exit 2; }
trap 'rm -rf "$STAGE"' EXIT
cp "$MANIFEST" "$STAGE/manifest.toml" || { echo "FATAL: could not stage manifest.toml"; exit 2; }
mkdir -p "$STAGE/strings" "$STAGE/$WIN_TRIPLE" "$STAGE/$GNU_TRIPLE" "$STAGE/$MUSL_TRIPLE" \
  || { echo "FATAL: could not create archive staging directories"; exit 2; }
for rel in $SHARED_FILES; do
  mkdir -p "$(dirname "$STAGE/strings/$rel")" \
    || { echo "FATAL: could not stage strings/$rel parent"; exit 2; }
  cp "$STRINGS/$rel" "$STAGE/strings/$rel" \
    || { echo "FATAL: could not stage strings/$rel"; exit 2; }
done
for b in $BINS; do
  cp "$(win_path "$b")" "$STAGE/$WIN_TRIPLE/$b.exe" \
    || { echo "FATAL: could not stage $WIN_TRIPLE/$b.exe"; exit 2; }
  cp "$(gnu_path "$b")" "$STAGE/$GNU_TRIPLE/$b" \
    || { echo "FATAL: could not stage $GNU_TRIPLE/$b"; exit 2; }
  cp "$(musl_path "$b")" "$STAGE/$MUSL_TRIPLE/$b" \
    || { echo "FATAL: could not stage $MUSL_TRIPLE/$b"; exit 2; }
done

mkdir -p "$(dirname "$OUT")" || { echo "FATAL: cannot create $(dirname "$OUT")"; exit 2; }
"$PY" "$ARCHIVE_WRITER" "$STAGE" "$OUT" "$WIN_TRIPLE" "$GNU_TRIPLE" "$MUSL_TRIPLE" \
  || { echo "FATAL: portable archive writer failed"; exit 2; }
# Self-validate the completed artifact: exact deterministic membership, portable modes, target
# formats/linkage, and byte-for-byte identity with every staged release input.
echo
echo "== validate archive =="
if ! listing=$(tar -tzf "$OUT"); then
  echo "FATAL: could not list completed archive" >&2
  exit 2
fi
expected=$(printf '%s\n' \
  manifest.toml \
  strings \
  strings/omp-spt.mjs \
  strings/package.json \
  strings/skills \
  strings/skills/commune \
  strings/skills/commune/SKILL.md \
  strings/skills/knock \
  strings/skills/knock/SKILL.md \
  strings/skills/role \
  strings/skills/role/SKILL.md \
  strings/skills/setup \
  strings/skills/setup/SKILL.md \
  strings/skills/signoff \
  strings/skills/signoff/SKILL.md \
  "$WIN_TRIPLE" "$WIN_TRIPLE/omp-spt.exe" \
  "$GNU_TRIPLE" "$GNU_TRIPLE/omp-spt" \
  "$MUSL_TRIPLE" "$MUSL_TRIPLE/omp-spt" | sort)
normalized=$(printf '%s\n' "$listing" | sed 's:/$::' | sort)
fatal=0
if [ "$normalized" != "$expected" ]; then
  echo "FATAL: archive membership differs from the exact release contract" >&2
  echo "expected: [$expected]" >&2
  echo "actual:   [$normalized]" >&2
  fatal=1
fi

VERIFY="$STAGE/.archive-verify"
mkdir -p "$VERIFY"
verify_exact() {
  source=$1
  member=$2
  extracted="$VERIFY/$member"
  mkdir -p "$(dirname "$extracted")"
  if ! tar -xOzf "$OUT" "$member" >"$extracted" 2>/dev/null; then
    echo "FATAL: could not extract $member for payload verification" >&2
    fatal=1
  elif ! cmp -s "$source" "$extracted"; then
    echo "FATAL: archived $member differs from its release input" >&2
    fatal=1
  fi
}
verify_exact "$MANIFEST" "manifest.toml"
for rel in $SHARED_FILES; do
  verify_exact "$STRINGS/$rel" "strings/$rel"
done
verify_exact "$(win_path omp-spt)" "$WIN_TRIPLE/omp-spt.exe"
verify_exact "$(gnu_path omp-spt)" "$GNU_TRIPLE/omp-spt"
verify_exact "$(musl_path omp-spt)" "$MUSL_TRIPLE/omp-spt"

win_magic=$(magic_of_file "$VERIFY/$WIN_TRIPLE/omp-spt.exe")
gnu_magic=$(magic_of_file "$VERIFY/$GNU_TRIPLE/omp-spt")
musl_magic=$(magic_of_file "$VERIFY/$MUSL_TRIPLE/omp-spt")
case "$win_magic" in 4d5a*) : ;; *) echo "FATAL: archived Windows payload is not PE" >&2; fatal=1 ;; esac
[ "$gnu_magic" = "7f454c46" ] || { echo "FATAL: archived GNU payload is not ELF" >&2; fatal=1; }
[ "$musl_magic" = "7f454c46" ] || { echo "FATAL: archived musl payload is not ELF" >&2; fatal=1; }
for triple in "$GNU_TRIPLE" "$MUSL_TRIPLE"; do
  mode=$(tar -tvzf "$OUT" "$triple/omp-spt" 2>/dev/null | awk 'NR == 1 {print $1}')
  if [ "$mode" != "-rwxr-xr-x" ]; then
    echo "FATAL: archived $triple payload mode is not canonical 0755 ($mode)" >&2
    fatal=1
  fi
done
"$PY" "$BINARY_VALIDATOR" "$WIN_TRIPLE" "$VERIFY/$WIN_TRIPLE/omp-spt.exe" || fatal=1
"$PY" "$BINARY_VALIDATOR" "$GNU_TRIPLE" "$VERIFY/$GNU_TRIPLE/omp-spt" || fatal=1
"$PY" "$BINARY_VALIDATOR" "$MUSL_TRIPLE" "$VERIFY/$MUSL_TRIPLE/omp-spt" || fatal=1
[ "$fatal" -eq 0 ] || exit 1
echo "ok   exact shared plugin + PE/GNU ELF/static musl payloads; Linux modes fixed at 0755"

echo
echo "WROTE $OUT (single fat adapter.spt). Next (operator): attach as 'adapter.spt' to a GitHub release"
echo "on BigscreenVR/omp-spt; end users 'spt adapter add --release BigscreenVR/omp-spt' (spt"
echo "v$floor+). The fat archive auto-resolves one of the three target helpers."
exit 0
